From fae52e5a77e97ab8eda60b9a585d05b24147a5cc Mon Sep 17 00:00:00 2001 From: Razvan Musaloiu-E Date: Fri, 28 Dec 2018 21:50:10 -0800 Subject: [PATCH 1/5] Add back the Device Manager from x/ref/services/device/ I did not include the x/services/device/dmrun because it has some depencies on jiri that I need to look into. This also doesn't include any of the other daemons that the device managed --- v23/services/application/application.go | 82 + v23/services/application/application.vdl | 81 + v23/services/application/application.vdl.go | 500 +++ v23/services/application/application_test.go | 68 + v23/services/binary/binary.vdl | 39 + v23/services/binary/binary.vdl.go | 294 ++ v23/services/build/build.vdl | 48 + v23/services/build/build.vdl.go | 724 +++++ v23/services/build/util.go | 54 + v23/services/device/device.vdl | 366 +++ v23/services/device/device.vdl.go | 2834 +++++++++++++++++ v23/services/repository/repository.vdl | 124 + v23/services/repository/repository.vdl.go | 1242 ++++++++ v23/services/tidyable/service.vdl | 18 + v23/services/tidyable/tidyable.vdl.go | 151 + .../device/claimable/claimable_v23_test.go | 88 + x/ref/services/device/claimable/doc.go | 82 + x/ref/services/device/claimable/main.go | 98 + x/ref/services/device/config.vdl | 11 + x/ref/services/device/device.vdl.go | 148 + x/ref/services/device/device/acl.go | 146 + x/ref/services/device/device/acl_fmt.go | 86 + x/ref/services/device/device/acl_test.go | 302 ++ x/ref/services/device/device/associate.go | 93 + .../services/device/device/associate_test.go | 163 + x/ref/services/device/device/claim.go | 68 + x/ref/services/device/device/claim_test.go | 116 + x/ref/services/device/device/debug.go | 38 + x/ref/services/device/device/debug_test.go | 56 + x/ref/services/device/device/delete.go | 37 + x/ref/services/device/device/delete_test.go | 11 + x/ref/services/device/device/describe.go | 37 + .../device/device/devicemanager_mock_test.go | 315 ++ x/ref/services/device/device/doc.go | 523 +++ x/ref/services/device/device/glob.go | 505 +++ x/ref/services/device/device/glob_test.go | 527 +++ x/ref/services/device/device/install.go | 83 + x/ref/services/device/device/install_test.go | 133 + x/ref/services/device/device/instantiate.go | 83 + .../device/device/instantiate_test.go | 100 + x/ref/services/device/device/kill.go | 40 + x/ref/services/device/device/kill_test.go | 91 + x/ref/services/device/device/local_install.go | 306 ++ .../device/device/local_install_test.go | 257 ++ x/ref/services/device/device/ls.go | 32 + x/ref/services/device/device/ls_test.go | 160 + x/ref/services/device/device/publish.go | 261 ++ x/ref/services/device/device/root.go | 29 + x/ref/services/device/device/run.go | 37 + x/ref/services/device/device/run_test.go | 11 + x/ref/services/device/device/status.go | 41 + x/ref/services/device/device/status_test.go | 76 + x/ref/services/device/device/uninstall.go | 38 + x/ref/services/device/device/update.go | 161 + x/ref/services/device/device/update_test.go | 159 + x/ref/services/device/device/util_test.go | 126 + x/ref/services/device/deviced/commands.go | 169 + x/ref/services/device/deviced/doc.go | 217 ++ .../deviced/internal/impl/app_service.go | 1692 ++++++++++ .../internal/impl/app_starting_util.go | 144 + .../device/deviced/internal/impl/app_state.go | 91 + .../deviced/internal/impl/app_state_test.go | 97 + .../internal/impl/applife/app_life_test.go | 667 ++++ .../internal/impl/applife/impl_test.go | 19 + .../impl/applife/instance_reaping_test.go | 80 + .../internal/impl/associate_instance_test.go | 32 + .../internal/impl/association_instance.go | 34 + .../internal/impl/association_state.go | 128 + .../internal/impl/association_state_test.go | 170 + .../device/deviced/internal/impl/callback.go | 33 + .../deviced/internal/impl/config_service.go | 159 + .../impl/daemonreap/daemon_reaping_test.go | 86 + .../internal/impl/daemonreap/impl_test.go | 19 + .../daemonreap/instance_reaping_kill_test.go | 111 + .../daemonreap/persistent_daemon_kill_test.go | 80 + .../deviced/internal/impl/device_service.go | 587 ++++ .../deviced/internal/impl/dispatcher.go | 377 +++ .../impl/globsuid/args_darwin_test.go | 10 + .../internal/impl/globsuid/args_linux_test.go | 10 + .../internal/impl/globsuid/glob_test.go | 112 + .../internal/impl/globsuid/impl_test.go | 19 + .../impl/globsuid/signature_match_test.go | 161 + .../internal/impl/globsuid/suid_test.go | 203 ++ .../deviced/internal/impl/helper_manager.go | 200 ++ .../deviced/internal/impl/impl_helper_test.go | 55 + .../device/deviced/internal/impl/impl_test.go | 598 ++++ .../deviced/internal/impl/instance_reaping.go | 292 ++ .../deviced/internal/impl/only_for_test.go | 31 + .../internal/impl/perms/debug_perms_test.go | 264 ++ .../deviced/internal/impl/perms/impl_test.go | 19 + .../deviced/internal/impl/perms/perms_test.go | 197 ++ .../deviced/internal/impl/perms_propagator.go | 45 + .../internal/impl/principal_manager.go | 92 + .../device/deviced/internal/impl/profile.go | 196 ++ .../deviced/internal/impl/proxy_invoker.go | 251 ++ .../internal/impl/proxy_invoker_test.go | 78 + .../deviced/internal/impl/restart_policy.go | 55 + .../internal/impl/restart_policy_test.go | 135 + .../deviced/internal/impl/shell_android.go | 10 + .../deviced/internal/impl/shell_darwin.go | 10 + .../deviced/internal/impl/shell_linux.go | 12 + .../device/deviced/internal/impl/stats.go | 68 + .../device/deviced/internal/impl/tidyup.go | 257 ++ .../device/deviced/internal/impl/util.go | 249 ++ .../deviced/internal/impl/utiltest/app.go | 204 ++ .../deviced/internal/impl/utiltest/helpers.go | 908 ++++++ .../internal/impl/utiltest/mock_repo.go | 191 ++ .../deviced/internal/impl/utiltest/modules.go | 177 + .../internal/installer/device_installer.go | 415 +++ .../deviced/internal/starter/starter.go | 307 ++ .../internal/versioning/creator_info.go | 85 + x/ref/services/device/deviced/main.go | 42 + x/ref/services/device/deviced/server.go | 148 + x/ref/services/device/inithelper/main.go | 111 + x/ref/services/device/internal/claim/claim.go | 125 + .../services/device/internal/config/config.go | 165 + .../device/internal/config/config_test.go | 135 + .../services/device/internal/config/const.go | 28 + .../services/device/internal/errors/errors.go | 31 + x/ref/services/device/internal/suid/args.go | 234 ++ .../device/internal/suid/args_test.go | 207 ++ .../device/internal/suid/constants.go | 11 + x/ref/services/device/internal/suid/run.go | 36 + x/ref/services/device/internal/suid/system.go | 140 + .../device/internal/suid/system_test.go | 74 + .../device/internal/sysinit/init_darwin.go | 13 + .../device/internal/sysinit/init_linux.go | 319 ++ .../device/internal/sysinit/linux_test.go | 129 + .../internal/sysinit/service_description.go | 85 + .../device/internal/sysinit/sysinit.go | 17 + x/ref/services/device/mgmt_v23_test.go | 646 ++++ x/ref/services/device/restarter/doc.go | 38 + x/ref/services/device/restarter/main.go | 133 + x/ref/services/device/restarter/v23_test.go | 112 + x/ref/services/device/suidhelper/main.go | 29 + x/ref/services/device/util_darwin_test.go | 90 + x/ref/services/device/util_linux_test.go | 28 + x/ref/services/internal/binarylib/client.go | 363 +++ .../internal/binarylib/client_test.go | 192 ++ .../services/internal/binarylib/dispatcher.go | 63 + x/ref/services/internal/binarylib/fs_utils.go | 150 + x/ref/services/internal/binarylib/http.go | 54 + .../services/internal/binarylib/http_test.go | 86 + .../services/internal/binarylib/impl_test.go | 330 ++ .../services/internal/binarylib/perms_test.go | 473 +++ x/ref/services/internal/binarylib/service.go | 437 +++ x/ref/services/internal/binarylib/setup.go | 53 + x/ref/services/internal/binarylib/state.go | 86 + .../services/internal/binarylib/util_test.go | 95 + x/ref/services/internal/binarylib/v23_test.go | 15 + x/ref/services/internal/packages/packages.go | 302 ++ .../internal/packages/packages_test.go | 208 ++ .../internal/profiles/listprofiles.go | 92 + x/ref/services/profile/profile.vdl | 36 + x/ref/services/profile/profile.vdl.go | 368 +++ x/ref/services/repository/repository.vdl | 58 + x/ref/services/repository/repository.vdl.go | 435 +++ 157 files changed, 30229 insertions(+) create mode 100644 v23/services/application/application.go create mode 100644 v23/services/application/application.vdl create mode 100644 v23/services/application/application.vdl.go create mode 100644 v23/services/application/application_test.go create mode 100644 v23/services/binary/binary.vdl create mode 100644 v23/services/binary/binary.vdl.go create mode 100644 v23/services/build/build.vdl create mode 100644 v23/services/build/build.vdl.go create mode 100644 v23/services/build/util.go create mode 100644 v23/services/device/device.vdl create mode 100644 v23/services/device/device.vdl.go create mode 100644 v23/services/repository/repository.vdl create mode 100644 v23/services/repository/repository.vdl.go create mode 100644 v23/services/tidyable/service.vdl create mode 100644 v23/services/tidyable/tidyable.vdl.go create mode 100644 x/ref/services/device/claimable/claimable_v23_test.go create mode 100644 x/ref/services/device/claimable/doc.go create mode 100644 x/ref/services/device/claimable/main.go create mode 100644 x/ref/services/device/config.vdl create mode 100644 x/ref/services/device/device.vdl.go create mode 100644 x/ref/services/device/device/acl.go create mode 100644 x/ref/services/device/device/acl_fmt.go create mode 100644 x/ref/services/device/device/acl_test.go create mode 100644 x/ref/services/device/device/associate.go create mode 100644 x/ref/services/device/device/associate_test.go create mode 100644 x/ref/services/device/device/claim.go create mode 100644 x/ref/services/device/device/claim_test.go create mode 100644 x/ref/services/device/device/debug.go create mode 100644 x/ref/services/device/device/debug_test.go create mode 100644 x/ref/services/device/device/delete.go create mode 100644 x/ref/services/device/device/delete_test.go create mode 100644 x/ref/services/device/device/describe.go create mode 100644 x/ref/services/device/device/devicemanager_mock_test.go create mode 100644 x/ref/services/device/device/doc.go create mode 100644 x/ref/services/device/device/glob.go create mode 100644 x/ref/services/device/device/glob_test.go create mode 100644 x/ref/services/device/device/install.go create mode 100644 x/ref/services/device/device/install_test.go create mode 100644 x/ref/services/device/device/instantiate.go create mode 100644 x/ref/services/device/device/instantiate_test.go create mode 100644 x/ref/services/device/device/kill.go create mode 100644 x/ref/services/device/device/kill_test.go create mode 100644 x/ref/services/device/device/local_install.go create mode 100644 x/ref/services/device/device/local_install_test.go create mode 100644 x/ref/services/device/device/ls.go create mode 100644 x/ref/services/device/device/ls_test.go create mode 100644 x/ref/services/device/device/publish.go create mode 100644 x/ref/services/device/device/root.go create mode 100644 x/ref/services/device/device/run.go create mode 100644 x/ref/services/device/device/run_test.go create mode 100644 x/ref/services/device/device/status.go create mode 100644 x/ref/services/device/device/status_test.go create mode 100644 x/ref/services/device/device/uninstall.go create mode 100644 x/ref/services/device/device/update.go create mode 100644 x/ref/services/device/device/update_test.go create mode 100644 x/ref/services/device/device/util_test.go create mode 100644 x/ref/services/device/deviced/commands.go create mode 100644 x/ref/services/device/deviced/doc.go create mode 100644 x/ref/services/device/deviced/internal/impl/app_service.go create mode 100644 x/ref/services/device/deviced/internal/impl/app_starting_util.go create mode 100644 x/ref/services/device/deviced/internal/impl/app_state.go create mode 100644 x/ref/services/device/deviced/internal/impl/app_state_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/applife/app_life_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/applife/impl_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/applife/instance_reaping_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/associate_instance_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/association_instance.go create mode 100644 x/ref/services/device/deviced/internal/impl/association_state.go create mode 100644 x/ref/services/device/deviced/internal/impl/association_state_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/callback.go create mode 100644 x/ref/services/device/deviced/internal/impl/config_service.go create mode 100644 x/ref/services/device/deviced/internal/impl/daemonreap/daemon_reaping_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/daemonreap/impl_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/daemonreap/instance_reaping_kill_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/daemonreap/persistent_daemon_kill_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/device_service.go create mode 100644 x/ref/services/device/deviced/internal/impl/dispatcher.go create mode 100644 x/ref/services/device/deviced/internal/impl/globsuid/args_darwin_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/globsuid/args_linux_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/globsuid/glob_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/globsuid/impl_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/globsuid/signature_match_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/globsuid/suid_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/helper_manager.go create mode 100644 x/ref/services/device/deviced/internal/impl/impl_helper_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/impl_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/instance_reaping.go create mode 100644 x/ref/services/device/deviced/internal/impl/only_for_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/perms/debug_perms_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/perms/impl_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/perms/perms_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/perms_propagator.go create mode 100644 x/ref/services/device/deviced/internal/impl/principal_manager.go create mode 100644 x/ref/services/device/deviced/internal/impl/profile.go create mode 100644 x/ref/services/device/deviced/internal/impl/proxy_invoker.go create mode 100644 x/ref/services/device/deviced/internal/impl/proxy_invoker_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/restart_policy.go create mode 100644 x/ref/services/device/deviced/internal/impl/restart_policy_test.go create mode 100644 x/ref/services/device/deviced/internal/impl/shell_android.go create mode 100644 x/ref/services/device/deviced/internal/impl/shell_darwin.go create mode 100644 x/ref/services/device/deviced/internal/impl/shell_linux.go create mode 100644 x/ref/services/device/deviced/internal/impl/stats.go create mode 100644 x/ref/services/device/deviced/internal/impl/tidyup.go create mode 100644 x/ref/services/device/deviced/internal/impl/util.go create mode 100644 x/ref/services/device/deviced/internal/impl/utiltest/app.go create mode 100644 x/ref/services/device/deviced/internal/impl/utiltest/helpers.go create mode 100644 x/ref/services/device/deviced/internal/impl/utiltest/mock_repo.go create mode 100644 x/ref/services/device/deviced/internal/impl/utiltest/modules.go create mode 100644 x/ref/services/device/deviced/internal/installer/device_installer.go create mode 100644 x/ref/services/device/deviced/internal/starter/starter.go create mode 100644 x/ref/services/device/deviced/internal/versioning/creator_info.go create mode 100644 x/ref/services/device/deviced/main.go create mode 100644 x/ref/services/device/deviced/server.go create mode 100644 x/ref/services/device/inithelper/main.go create mode 100644 x/ref/services/device/internal/claim/claim.go create mode 100644 x/ref/services/device/internal/config/config.go create mode 100644 x/ref/services/device/internal/config/config_test.go create mode 100644 x/ref/services/device/internal/config/const.go create mode 100644 x/ref/services/device/internal/errors/errors.go create mode 100644 x/ref/services/device/internal/suid/args.go create mode 100644 x/ref/services/device/internal/suid/args_test.go create mode 100644 x/ref/services/device/internal/suid/constants.go create mode 100644 x/ref/services/device/internal/suid/run.go create mode 100644 x/ref/services/device/internal/suid/system.go create mode 100644 x/ref/services/device/internal/suid/system_test.go create mode 100644 x/ref/services/device/internal/sysinit/init_darwin.go create mode 100644 x/ref/services/device/internal/sysinit/init_linux.go create mode 100644 x/ref/services/device/internal/sysinit/linux_test.go create mode 100644 x/ref/services/device/internal/sysinit/service_description.go create mode 100644 x/ref/services/device/internal/sysinit/sysinit.go create mode 100644 x/ref/services/device/mgmt_v23_test.go create mode 100644 x/ref/services/device/restarter/doc.go create mode 100644 x/ref/services/device/restarter/main.go create mode 100644 x/ref/services/device/restarter/v23_test.go create mode 100644 x/ref/services/device/suidhelper/main.go create mode 100644 x/ref/services/device/util_darwin_test.go create mode 100644 x/ref/services/device/util_linux_test.go create mode 100644 x/ref/services/internal/binarylib/client.go create mode 100644 x/ref/services/internal/binarylib/client_test.go create mode 100644 x/ref/services/internal/binarylib/dispatcher.go create mode 100644 x/ref/services/internal/binarylib/fs_utils.go create mode 100644 x/ref/services/internal/binarylib/http.go create mode 100644 x/ref/services/internal/binarylib/http_test.go create mode 100644 x/ref/services/internal/binarylib/impl_test.go create mode 100644 x/ref/services/internal/binarylib/perms_test.go create mode 100644 x/ref/services/internal/binarylib/service.go create mode 100644 x/ref/services/internal/binarylib/setup.go create mode 100644 x/ref/services/internal/binarylib/state.go create mode 100644 x/ref/services/internal/binarylib/util_test.go create mode 100644 x/ref/services/internal/binarylib/v23_test.go create mode 100644 x/ref/services/internal/packages/packages.go create mode 100644 x/ref/services/internal/packages/packages_test.go create mode 100644 x/ref/services/internal/profiles/listprofiles.go create mode 100644 x/ref/services/profile/profile.vdl create mode 100644 x/ref/services/profile/profile.vdl.go create mode 100644 x/ref/services/repository/repository.vdl create mode 100644 x/ref/services/repository/repository.vdl.go diff --git a/v23/services/application/application.go b/v23/services/application/application.go new file mode 100644 index 000000000..8409e8df5 --- /dev/null +++ b/v23/services/application/application.go @@ -0,0 +1,82 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Make the Envelope type JSON-codeable. + +package application + +import ( + "encoding/base64" + "encoding/json" + "time" + + "v.io/v23/security" + "v.io/v23/verror" + "v.io/v23/vom" +) + +const pkgPath = "v.io/v23/services/application" + +var ( + errCantVOMEncodePublisher = verror.Register(pkgPath+".errCantVOMEncodePublisher", verror.NoRetry, "{1:}{2:} failed to vom-encode Publisher{:_}") + errCantBase64DecodePublisher = verror.Register(pkgPath+".errCantBase64DecodePublisher", verror.NoRetry, "{1:}{2:} failed to base64-decode Publisher{:_}") + errCantVOMDecodePublisher = verror.Register(pkgPath+".errCantVOMDecodePublisher", verror.NoRetry, "{1:}{2:} failed to vom-decode Publisher{:_}") +) + +type jsonType struct { + Title string + Args []string + Binary SignedFile + Publisher string // base64-vom-encoded security.Blessings + Env []string + Packages Packages + Restarts int32 + RestartTimeWindow time.Duration +} + +func (env Envelope) MarshalJSON() ([]byte, error) { + var bytes []byte + if !env.Publisher.IsZero() { + var err error + if bytes, err = vom.Encode(env.Publisher); err != nil { + return nil, verror.New(errCantVOMEncodePublisher, nil, err) + } + } + return json.Marshal(jsonType{ + Title: env.Title, + Args: env.Args, + Binary: env.Binary, + Publisher: base64.URLEncoding.EncodeToString(bytes), + Env: env.Env, + Packages: env.Packages, + Restarts: env.Restarts, + RestartTimeWindow: env.RestartTimeWindow, + }) +} + +func (env *Envelope) UnmarshalJSON(input []byte) error { + var jt jsonType + if err := json.Unmarshal(input, &jt); err != nil { + return err + } + var publisher security.Blessings + if len(jt.Publisher) > 0 { + bytes, err := base64.URLEncoding.DecodeString(jt.Publisher) + if err != nil { + return verror.New(errCantBase64DecodePublisher, nil, err) + } + if err := vom.Decode(bytes, &publisher); err != nil { + return verror.New(errCantVOMDecodePublisher, nil, err) + } + } + env.Title = jt.Title + env.Args = jt.Args + env.Binary = jt.Binary + env.Publisher = publisher + env.Env = jt.Env + env.Packages = jt.Packages + env.Restarts = jt.Restarts + env.RestartTimeWindow = jt.RestartTimeWindow + return nil +} diff --git a/v23/services/application/application.vdl b/v23/services/application/application.vdl new file mode 100644 index 000000000..a47527f0f --- /dev/null +++ b/v23/services/application/application.vdl @@ -0,0 +1,81 @@ +// Copyright 2015 The Vanadium 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 application defines types for describing applications. +package application + +import ( + "time" + "v.io/v23/security" +) + +// Device manager application envelopes must present this title. +const DeviceManagerTitle = "device manager" + +// Envelope is a collection of metadata that describes an application. +type Envelope struct { + // Title is the publisher-assigned application title. Application + // installations with the same title are considered as belonging to the + // same application by the application management system. + // + // A change in the title signals a new application. + Title string + // Args is an array of command-line arguments to be used when executing + // the binary. + Args []string + // Binary identifies the application binary. + Binary SignedFile + // Publisher represents the set of blessings that have been bound to + // the principal who published this binary. + Publisher security.WireBlessings + // Env is an array that stores the environment variable values to be + // used when executing the binary. + Env []string + // Packages is the set of packages to install on the local filesystem + // before executing the binary + Packages Packages + // Restarts specifies how many times the device manager will attempt + // to automatically restart an application that has crashed before + // giving up and marking the application as NotRunning. + Restarts int32 + // RestartTimeWindow is the time window within which an + // application exit is considered a crash that counts against the + // Restarts budget. If the application crashes after less than + // RestartTimeWindow time for Restarts consecutive times, the + // application is marked NotRunning and no more restart attempts + // are made. If the application has run continuously for more + // than RestartTimeWindow, subsequent crashes will again benefit + // from up to Restarts restarts (that is, the Restarts budget is + // reset by a successful run of at least RestartTimeWindow + // duration). + RestartTimeWindow time.Duration +} + +// Packages represents a set of packages. The map key is the local +// file/directory name, relative to the instance's packages directory, where the +// package should be installed. For archives, this name represents a directory +// into which the archive is to be extracted, and for regular files it +// represents the name for the file. The map value is the package +// specification. +// +// Each object's media type determines how to install it. +// +// For example, with key=pkg1,value=SignedFile{File:binaryrepo/configfiles} (an +// archive), the "configfiles" package will be installed under the "pkg1" +// directory. With key=pkg2,value=SignedFile{File:binaryrepo/binfile} (a +// binary), the "binfile" file will be installed as the "pkg2" file. +// +// The keys must be valid file/directory names, without path separators. +// +// Any number of packages may be specified. +type Packages map[string]SignedFile + +// SignedFile represents a file accompanied by a signature of its contents. +type SignedFile struct { + // File is the object name of the file. + File string + // Signature represents a signature on the sha256 hash of the file + // contents by the publisher principal. + Signature security.Signature +} diff --git a/v23/services/application/application.vdl.go b/v23/services/application/application.vdl.go new file mode 100644 index 000000000..f3f6aa7d4 --- /dev/null +++ b/v23/services/application/application.vdl.go @@ -0,0 +1,500 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated by the vanadium vdl tool. +// Package: application + +// Package application defines types for describing applications. +package application + +import ( + "time" + "v.io/v23/security" + "v.io/v23/vdl" + vdltime "v.io/v23/vdlroot/time" +) + +var _ = __VDLInit() // Must be first; see __VDLInit comments for details. + +////////////////////////////////////////////////// +// Type definitions + +// SignedFile represents a file accompanied by a signature of its contents. +type SignedFile struct { + // File is the object name of the file. + File string + // Signature represents a signature on the sha256 hash of the file + // contents by the publisher principal. + Signature security.Signature +} + +func (SignedFile) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/application.SignedFile"` +}) { +} + +func (x SignedFile) VDLIsZero() bool { + if x.File != "" { + return false + } + if !x.Signature.VDLIsZero() { + return false + } + return true +} + +func (x SignedFile) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_1); err != nil { + return err + } + if x.File != "" { + if err := enc.NextFieldValueString(0, vdl.StringType, x.File); err != nil { + return err + } + } + if !x.Signature.VDLIsZero() { + if err := enc.NextField(1); err != nil { + return err + } + if err := x.Signature.VDLWrite(enc); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *SignedFile) VDLRead(dec vdl.Decoder) error { + *x = SignedFile{} + if err := dec.StartValue(__VDLType_struct_1); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_1 { + index = __VDLType_struct_1.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.File = value + } + case 1: + if err := x.Signature.VDLRead(dec); err != nil { + return err + } + } + } +} + +// Packages represents a set of packages. The map key is the local +// file/directory name, relative to the instance's packages directory, where the +// package should be installed. For archives, this name represents a directory +// into which the archive is to be extracted, and for regular files it +// represents the name for the file. The map value is the package +// specification. +// +// Each object's media type determines how to install it. +// +// For example, with key=pkg1,value=SignedFile{File:binaryrepo/configfiles} (an +// archive), the "configfiles" package will be installed under the "pkg1" +// directory. With key=pkg2,value=SignedFile{File:binaryrepo/binfile} (a +// binary), the "binfile" file will be installed as the "pkg2" file. +// +// The keys must be valid file/directory names, without path separators. +// +// Any number of packages may be specified. +type Packages map[string]SignedFile + +func (Packages) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/application.Packages"` +}) { +} + +func (x Packages) VDLIsZero() bool { + return len(x) == 0 +} + +func (x Packages) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_map_3); err != nil { + return err + } + if err := enc.SetLenHint(len(x)); err != nil { + return err + } + for key, elem := range x { + if err := enc.NextEntryValueString(vdl.StringType, key); err != nil { + return err + } + if err := elem.VDLWrite(enc); err != nil { + return err + } + } + if err := enc.NextEntry(true); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *Packages) VDLRead(dec vdl.Decoder) error { + if err := dec.StartValue(__VDLType_map_3); err != nil { + return err + } + var tmpMap Packages + if len := dec.LenHint(); len > 0 { + tmpMap = make(Packages, len) + } + for { + switch done, key, err := dec.NextEntryValueString(); { + case err != nil: + return err + case done: + *x = tmpMap + return dec.FinishValue() + default: + var elem SignedFile + if err := elem.VDLRead(dec); err != nil { + return err + } + if tmpMap == nil { + tmpMap = make(Packages) + } + tmpMap[key] = elem + } + } +} + +// Envelope is a collection of metadata that describes an application. +type Envelope struct { + // Title is the publisher-assigned application title. Application + // installations with the same title are considered as belonging to the + // same application by the application management system. + // + // A change in the title signals a new application. + Title string + // Args is an array of command-line arguments to be used when executing + // the binary. + Args []string + // Binary identifies the application binary. + Binary SignedFile + // Publisher represents the set of blessings that have been bound to + // the principal who published this binary. + Publisher security.Blessings + // Env is an array that stores the environment variable values to be + // used when executing the binary. + Env []string + // Packages is the set of packages to install on the local filesystem + // before executing the binary + Packages Packages + // Restarts specifies how many times the device manager will attempt + // to automatically restart an application that has crashed before + // giving up and marking the application as NotRunning. + Restarts int32 + // RestartTimeWindow is the time window within which an + // application exit is considered a crash that counts against the + // Restarts budget. If the application crashes after less than + // RestartTimeWindow time for Restarts consecutive times, the + // application is marked NotRunning and no more restart attempts + // are made. If the application has run continuously for more + // than RestartTimeWindow, subsequent crashes will again benefit + // from up to Restarts restarts (that is, the Restarts budget is + // reset by a successful run of at least RestartTimeWindow + // duration). + RestartTimeWindow time.Duration +} + +func (Envelope) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/application.Envelope"` +}) { +} + +func (x Envelope) VDLIsZero() bool { + if x.Title != "" { + return false + } + if len(x.Args) != 0 { + return false + } + if !x.Binary.VDLIsZero() { + return false + } + if !x.Publisher.IsZero() { + return false + } + if len(x.Env) != 0 { + return false + } + if len(x.Packages) != 0 { + return false + } + if x.Restarts != 0 { + return false + } + if x.RestartTimeWindow != 0 { + return false + } + return true +} + +func (x Envelope) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_4); err != nil { + return err + } + if x.Title != "" { + if err := enc.NextFieldValueString(0, vdl.StringType, x.Title); err != nil { + return err + } + } + if len(x.Args) != 0 { + if err := enc.NextField(1); err != nil { + return err + } + if err := __VDLWriteAnon_list_1(enc, x.Args); err != nil { + return err + } + } + if !x.Binary.VDLIsZero() { + if err := enc.NextField(2); err != nil { + return err + } + if err := x.Binary.VDLWrite(enc); err != nil { + return err + } + } + if !x.Publisher.IsZero() { + if err := enc.NextField(3); err != nil { + return err + } + var wire security.WireBlessings + if err := security.WireBlessingsFromNative(&wire, x.Publisher); err != nil { + return err + } + if err := wire.VDLWrite(enc); err != nil { + return err + } + } + if len(x.Env) != 0 { + if err := enc.NextField(4); err != nil { + return err + } + if err := __VDLWriteAnon_list_1(enc, x.Env); err != nil { + return err + } + } + if len(x.Packages) != 0 { + if err := enc.NextField(5); err != nil { + return err + } + if err := x.Packages.VDLWrite(enc); err != nil { + return err + } + } + if x.Restarts != 0 { + if err := enc.NextFieldValueInt(6, vdl.Int32Type, int64(x.Restarts)); err != nil { + return err + } + } + if x.RestartTimeWindow != 0 { + if err := enc.NextField(7); err != nil { + return err + } + var wire vdltime.Duration + if err := vdltime.DurationFromNative(&wire, x.RestartTimeWindow); err != nil { + return err + } + if err := wire.VDLWrite(enc); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func __VDLWriteAnon_list_1(enc vdl.Encoder, x []string) error { + if err := enc.StartValue(__VDLType_list_5); err != nil { + return err + } + if err := enc.SetLenHint(len(x)); err != nil { + return err + } + for _, elem := range x { + if err := enc.NextEntryValueString(vdl.StringType, elem); err != nil { + return err + } + } + if err := enc.NextEntry(true); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *Envelope) VDLRead(dec vdl.Decoder) error { + *x = Envelope{} + if err := dec.StartValue(__VDLType_struct_4); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_4 { + index = __VDLType_struct_4.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.Title = value + } + case 1: + if err := __VDLReadAnon_list_1(dec, &x.Args); err != nil { + return err + } + case 2: + if err := x.Binary.VDLRead(dec); err != nil { + return err + } + case 3: + var wire security.WireBlessings + if err := wire.VDLRead(dec); err != nil { + return err + } + if err := security.WireBlessingsToNative(wire, &x.Publisher); err != nil { + return err + } + case 4: + if err := __VDLReadAnon_list_1(dec, &x.Env); err != nil { + return err + } + case 5: + if err := x.Packages.VDLRead(dec); err != nil { + return err + } + case 6: + switch value, err := dec.ReadValueInt(32); { + case err != nil: + return err + default: + x.Restarts = int32(value) + } + case 7: + var wire vdltime.Duration + if err := wire.VDLRead(dec); err != nil { + return err + } + if err := vdltime.DurationToNative(wire, &x.RestartTimeWindow); err != nil { + return err + } + } + } +} + +func __VDLReadAnon_list_1(dec vdl.Decoder, x *[]string) error { + if err := dec.StartValue(__VDLType_list_5); err != nil { + return err + } + if len := dec.LenHint(); len > 0 { + *x = make([]string, 0, len) + } else { + *x = nil + } + for { + switch done, elem, err := dec.NextEntryValueString(); { + case err != nil: + return err + case done: + return dec.FinishValue() + default: + *x = append(*x, elem) + } + } +} + +////////////////////////////////////////////////// +// Const definitions + +// Device manager application envelopes must present this title. +const DeviceManagerTitle = "device manager" + +// Hold type definitions in package-level variables, for better performance. +var ( + __VDLType_struct_1 *vdl.Type + __VDLType_struct_2 *vdl.Type + __VDLType_map_3 *vdl.Type + __VDLType_struct_4 *vdl.Type + __VDLType_list_5 *vdl.Type + __VDLType_struct_6 *vdl.Type + __VDLType_struct_7 *vdl.Type +) + +var __VDLInitCalled bool + +// __VDLInit performs vdl initialization. It is safe to call multiple times. +// If you have an init ordering issue, just insert the following line verbatim +// into your source files in this package, right after the "package foo" clause: +// +// var _ = __VDLInit() +// +// The purpose of this function is to ensure that vdl initialization occurs in +// the right order, and very early in the init sequence. In particular, vdl +// registration and package variable initialization needs to occur before +// functions like vdl.TypeOf will work properly. +// +// This function returns a dummy value, so that it can be used to initialize the +// first var in the file, to take advantage of Go's defined init order. +func __VDLInit() struct{} { + if __VDLInitCalled { + return struct{}{} + } + __VDLInitCalled = true + + // Register types. + vdl.Register((*SignedFile)(nil)) + vdl.Register((*Packages)(nil)) + vdl.Register((*Envelope)(nil)) + + // Initialize type definitions. + __VDLType_struct_1 = vdl.TypeOf((*SignedFile)(nil)).Elem() + __VDLType_struct_2 = vdl.TypeOf((*security.Signature)(nil)).Elem() + __VDLType_map_3 = vdl.TypeOf((*Packages)(nil)) + __VDLType_struct_4 = vdl.TypeOf((*Envelope)(nil)).Elem() + __VDLType_list_5 = vdl.TypeOf((*[]string)(nil)) + __VDLType_struct_6 = vdl.TypeOf((*security.WireBlessings)(nil)).Elem() + __VDLType_struct_7 = vdl.TypeOf((*vdltime.Duration)(nil)).Elem() + + return struct{}{} +} diff --git a/v23/services/application/application_test.go b/v23/services/application/application_test.go new file mode 100644 index 000000000..a1a845355 --- /dev/null +++ b/v23/services/application/application_test.go @@ -0,0 +1,68 @@ +// Copyright 2015 The Vanadium 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 application + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/json" + "reflect" + "testing" + "time" + + "v.io/v23/security" +) + +func TestEnvelopeJSON(t *testing.T) { + before := Envelope{ + Title: "title", + Args: []string{"arg1", "arg2"}, + Binary: SignedFile{File: "binary"}, + Publisher: newBlessing(t, "publisher"), + Env: []string{"NAME=value"}, + Packages: map[string]SignedFile{ + "pkg1": SignedFile{File: "pkg1.data"}, + "pkg2": SignedFile{File: "pkg2.config"}, + }, + Restarts: 2, + RestartTimeWindow: time.Second, + } + // If the fields of Envelope have been changed, then the testdata above + // needs to be updated. This actually applies recursively to the types + // of the fields of Envelope. If you have ideas on how to test that, + // chime in. + if n := reflect.TypeOf(before).NumField(); n != 8 { + t.Errorf("It appears that fields have been added to or removed from Envelope but TestEnvelopeJSON has not been updated. Please update it.") + } + + jsonBytes, err := json.Marshal(before) + if err != nil { + t.Fatal(err) + } + var after Envelope + if err := json.Unmarshal(jsonBytes, &after); err != nil { + t.Fatalf("Unmarshal(%q): %v", jsonBytes, err) + } + if !reflect.DeepEqual(before, after) { + t.Errorf("Got %#v, want %#v after JSON roundtripping", after, before) + } +} + +func newBlessing(t *testing.T, name string) security.Blessings { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + p, err := security.CreatePrincipal(security.NewInMemoryECDSASigner(key), nil, nil) + if err != nil { + t.Fatal(err) + } + b, err := p.BlessSelf(name) + if err != nil { + t.Fatal(err) + } + return b +} diff --git a/v23/services/binary/binary.vdl b/v23/services/binary/binary.vdl new file mode 100644 index 000000000..5524a1621 --- /dev/null +++ b/v23/services/binary/binary.vdl @@ -0,0 +1,39 @@ +// Copyright 2015 The Vanadium 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 binary defines types for describing executable binaries. +package binary + +const MissingChecksum = "" +const MissingSize = int64(-1) + +// Description describes a binary. Binaries are named and have been +// determined to run on some set of profiles. The mechanism for +// determing profiles is specifically not specified and left to the +// implementation of the interface that generates the description. +type Description struct { + // Name is the Object name of the application binary that can + // be used to fetch the actual binary from a content server. + Name string + // Profiles is a set of names of compatible profiles. Each + // name can either be an Object name that resolves to a + // Profile, or can be the profile's label, e.g.: + // + // "profiles/google/cluster/diskfull" + // "linux-media" + // + // Application developers can specify compatible profiles by + // hand, but we also want to be able to automatically derive + // the matching profiles from examining the binary itself + // (e.g. that's what Build.Describe() does). + Profiles map[string]bool +} + +// PartInfo holds information describing a binary part. +type PartInfo struct { + // Checksum holds the hex-encoded MD5 checksum of the binary part. + Checksum string + // Size holds the binary part size in bytes. + Size int64 +} diff --git a/v23/services/binary/binary.vdl.go b/v23/services/binary/binary.vdl.go new file mode 100644 index 000000000..43f5bd33c --- /dev/null +++ b/v23/services/binary/binary.vdl.go @@ -0,0 +1,294 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated by the vanadium vdl tool. +// Package: binary + +// Package binary defines types for describing executable binaries. +package binary + +import ( + "v.io/v23/vdl" +) + +var _ = __VDLInit() // Must be first; see __VDLInit comments for details. + +////////////////////////////////////////////////// +// Type definitions + +// Description describes a binary. Binaries are named and have been +// determined to run on some set of profiles. The mechanism for +// determing profiles is specifically not specified and left to the +// implementation of the interface that generates the description. +type Description struct { + // Name is the Object name of the application binary that can + // be used to fetch the actual binary from a content server. + Name string + // Profiles is a set of names of compatible profiles. Each + // name can either be an Object name that resolves to a + // Profile, or can be the profile's label, e.g.: + // + // "profiles/google/cluster/diskfull" + // "linux-media" + // + // Application developers can specify compatible profiles by + // hand, but we also want to be able to automatically derive + // the matching profiles from examining the binary itself + // (e.g. that's what Build.Describe() does). + Profiles map[string]bool +} + +func (Description) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/binary.Description"` +}) { +} + +func (x Description) VDLIsZero() bool { + if x.Name != "" { + return false + } + if len(x.Profiles) != 0 { + return false + } + return true +} + +func (x Description) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_1); err != nil { + return err + } + if x.Name != "" { + if err := enc.NextFieldValueString(0, vdl.StringType, x.Name); err != nil { + return err + } + } + if len(x.Profiles) != 0 { + if err := enc.NextField(1); err != nil { + return err + } + if err := __VDLWriteAnon_map_1(enc, x.Profiles); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func __VDLWriteAnon_map_1(enc vdl.Encoder, x map[string]bool) error { + if err := enc.StartValue(__VDLType_map_2); err != nil { + return err + } + if err := enc.SetLenHint(len(x)); err != nil { + return err + } + for key, elem := range x { + if err := enc.NextEntryValueString(vdl.StringType, key); err != nil { + return err + } + if err := enc.WriteValueBool(vdl.BoolType, elem); err != nil { + return err + } + } + if err := enc.NextEntry(true); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *Description) VDLRead(dec vdl.Decoder) error { + *x = Description{} + if err := dec.StartValue(__VDLType_struct_1); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_1 { + index = __VDLType_struct_1.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.Name = value + } + case 1: + if err := __VDLReadAnon_map_1(dec, &x.Profiles); err != nil { + return err + } + } + } +} + +func __VDLReadAnon_map_1(dec vdl.Decoder, x *map[string]bool) error { + if err := dec.StartValue(__VDLType_map_2); err != nil { + return err + } + var tmpMap map[string]bool + if len := dec.LenHint(); len > 0 { + tmpMap = make(map[string]bool, len) + } + for { + switch done, key, err := dec.NextEntryValueString(); { + case err != nil: + return err + case done: + *x = tmpMap + return dec.FinishValue() + default: + var elem bool + switch value, err := dec.ReadValueBool(); { + case err != nil: + return err + default: + elem = value + } + if tmpMap == nil { + tmpMap = make(map[string]bool) + } + tmpMap[key] = elem + } + } +} + +// PartInfo holds information describing a binary part. +type PartInfo struct { + // Checksum holds the hex-encoded MD5 checksum of the binary part. + Checksum string + // Size holds the binary part size in bytes. + Size int64 +} + +func (PartInfo) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/binary.PartInfo"` +}) { +} + +func (x PartInfo) VDLIsZero() bool { + return x == PartInfo{} +} + +func (x PartInfo) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_3); err != nil { + return err + } + if x.Checksum != "" { + if err := enc.NextFieldValueString(0, vdl.StringType, x.Checksum); err != nil { + return err + } + } + if x.Size != 0 { + if err := enc.NextFieldValueInt(1, vdl.Int64Type, x.Size); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *PartInfo) VDLRead(dec vdl.Decoder) error { + *x = PartInfo{} + if err := dec.StartValue(__VDLType_struct_3); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_3 { + index = __VDLType_struct_3.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.Checksum = value + } + case 1: + switch value, err := dec.ReadValueInt(64); { + case err != nil: + return err + default: + x.Size = value + } + } + } +} + +////////////////////////////////////////////////// +// Const definitions + +const MissingChecksum = "" +const MissingSize = int64(-1) + +// Hold type definitions in package-level variables, for better performance. +var ( + __VDLType_struct_1 *vdl.Type + __VDLType_map_2 *vdl.Type + __VDLType_struct_3 *vdl.Type +) + +var __VDLInitCalled bool + +// __VDLInit performs vdl initialization. It is safe to call multiple times. +// If you have an init ordering issue, just insert the following line verbatim +// into your source files in this package, right after the "package foo" clause: +// +// var _ = __VDLInit() +// +// The purpose of this function is to ensure that vdl initialization occurs in +// the right order, and very early in the init sequence. In particular, vdl +// registration and package variable initialization needs to occur before +// functions like vdl.TypeOf will work properly. +// +// This function returns a dummy value, so that it can be used to initialize the +// first var in the file, to take advantage of Go's defined init order. +func __VDLInit() struct{} { + if __VDLInitCalled { + return struct{}{} + } + __VDLInitCalled = true + + // Register types. + vdl.Register((*Description)(nil)) + vdl.Register((*PartInfo)(nil)) + + // Initialize type definitions. + __VDLType_struct_1 = vdl.TypeOf((*Description)(nil)).Elem() + __VDLType_map_2 = vdl.TypeOf((*map[string]bool)(nil)) + __VDLType_struct_3 = vdl.TypeOf((*PartInfo)(nil)).Elem() + + return struct{}{} +} diff --git a/v23/services/build/build.vdl b/v23/services/build/build.vdl new file mode 100644 index 000000000..ddfd25431 --- /dev/null +++ b/v23/services/build/build.vdl @@ -0,0 +1,48 @@ +// Copyright 2015 The Vanadium 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 build defines interfaces for building executable binaries. +package build + +import ( + "v.io/v23/services/binary" +) + +// Architecture specifies the hardware architecture of a host. +type Architecture enum { + Amd64 + Arm + X86 +} + +// Format specifies the file format of a host. +type Format enum { + Elf + Mach + Pe +} + +// OperatingSystem specifies the operating system of a host. +type OperatingSystem enum { + Darwin + Linux + Windows + Android +} + +// File records the name and contents of a file. +type File struct { + Name string + Contents []byte +} + +// Builder describes an interface for building binaries from source. +type Builder interface { + // Build streams sources to the build server, which then attempts to + // build the sources and streams back the compiled binaries. + Build(arch Architecture, os OperatingSystem) stream ([]byte | error) + // Describe generates a description for a binary identified by + // the given Object name. + Describe(name string) (binary.Description | error) +} diff --git a/v23/services/build/build.vdl.go b/v23/services/build/build.vdl.go new file mode 100644 index 000000000..a5708cb12 --- /dev/null +++ b/v23/services/build/build.vdl.go @@ -0,0 +1,724 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated by the vanadium vdl tool. +// Package: build + +// Package build defines interfaces for building executable binaries. +package build + +import ( + "fmt" + "io" + "v.io/v23" + "v.io/v23/context" + "v.io/v23/rpc" + "v.io/v23/services/binary" + "v.io/v23/vdl" +) + +var _ = __VDLInit() // Must be first; see __VDLInit comments for details. + +////////////////////////////////////////////////// +// Type definitions + +// Architecture specifies the hardware architecture of a host. +type Architecture int + +const ( + ArchitectureAmd64 Architecture = iota + ArchitectureArm + ArchitectureX86 +) + +// ArchitectureAll holds all labels for Architecture. +var ArchitectureAll = [...]Architecture{ArchitectureAmd64, ArchitectureArm, ArchitectureX86} + +// ArchitectureFromString creates a Architecture from a string label. +func ArchitectureFromString(label string) (x Architecture, err error) { + err = x.Set(label) + return +} + +// Set assigns label to x. +func (x *Architecture) Set(label string) error { + switch label { + case "Amd64", "amd64": + *x = ArchitectureAmd64 + return nil + case "Arm", "arm": + *x = ArchitectureArm + return nil + case "X86", "x86": + *x = ArchitectureX86 + return nil + } + *x = -1 + return fmt.Errorf("unknown label %q in build.Architecture", label) +} + +// String returns the string label of x. +func (x Architecture) String() string { + switch x { + case ArchitectureAmd64: + return "Amd64" + case ArchitectureArm: + return "Arm" + case ArchitectureX86: + return "X86" + } + return "" +} + +func (Architecture) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/build.Architecture"` + Enum struct{ Amd64, Arm, X86 string } +}) { +} + +func (x Architecture) VDLIsZero() bool { + return x == ArchitectureAmd64 +} + +func (x Architecture) VDLWrite(enc vdl.Encoder) error { + if err := enc.WriteValueString(__VDLType_enum_1, x.String()); err != nil { + return err + } + return nil +} + +func (x *Architecture) VDLRead(dec vdl.Decoder) error { + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + if err := x.Set(value); err != nil { + return err + } + } + return nil +} + +// Format specifies the file format of a host. +type Format int + +const ( + FormatElf Format = iota + FormatMach + FormatPe +) + +// FormatAll holds all labels for Format. +var FormatAll = [...]Format{FormatElf, FormatMach, FormatPe} + +// FormatFromString creates a Format from a string label. +func FormatFromString(label string) (x Format, err error) { + err = x.Set(label) + return +} + +// Set assigns label to x. +func (x *Format) Set(label string) error { + switch label { + case "Elf", "elf": + *x = FormatElf + return nil + case "Mach", "mach": + *x = FormatMach + return nil + case "Pe", "pe": + *x = FormatPe + return nil + } + *x = -1 + return fmt.Errorf("unknown label %q in build.Format", label) +} + +// String returns the string label of x. +func (x Format) String() string { + switch x { + case FormatElf: + return "Elf" + case FormatMach: + return "Mach" + case FormatPe: + return "Pe" + } + return "" +} + +func (Format) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/build.Format"` + Enum struct{ Elf, Mach, Pe string } +}) { +} + +func (x Format) VDLIsZero() bool { + return x == FormatElf +} + +func (x Format) VDLWrite(enc vdl.Encoder) error { + if err := enc.WriteValueString(__VDLType_enum_2, x.String()); err != nil { + return err + } + return nil +} + +func (x *Format) VDLRead(dec vdl.Decoder) error { + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + if err := x.Set(value); err != nil { + return err + } + } + return nil +} + +// OperatingSystem specifies the operating system of a host. +type OperatingSystem int + +const ( + OperatingSystemDarwin OperatingSystem = iota + OperatingSystemLinux + OperatingSystemWindows + OperatingSystemAndroid +) + +// OperatingSystemAll holds all labels for OperatingSystem. +var OperatingSystemAll = [...]OperatingSystem{OperatingSystemDarwin, OperatingSystemLinux, OperatingSystemWindows, OperatingSystemAndroid} + +// OperatingSystemFromString creates a OperatingSystem from a string label. +func OperatingSystemFromString(label string) (x OperatingSystem, err error) { + err = x.Set(label) + return +} + +// Set assigns label to x. +func (x *OperatingSystem) Set(label string) error { + switch label { + case "Darwin", "darwin": + *x = OperatingSystemDarwin + return nil + case "Linux", "linux": + *x = OperatingSystemLinux + return nil + case "Windows", "windows": + *x = OperatingSystemWindows + return nil + case "Android", "android": + *x = OperatingSystemAndroid + return nil + } + *x = -1 + return fmt.Errorf("unknown label %q in build.OperatingSystem", label) +} + +// String returns the string label of x. +func (x OperatingSystem) String() string { + switch x { + case OperatingSystemDarwin: + return "Darwin" + case OperatingSystemLinux: + return "Linux" + case OperatingSystemWindows: + return "Windows" + case OperatingSystemAndroid: + return "Android" + } + return "" +} + +func (OperatingSystem) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/build.OperatingSystem"` + Enum struct{ Darwin, Linux, Windows, Android string } +}) { +} + +func (x OperatingSystem) VDLIsZero() bool { + return x == OperatingSystemDarwin +} + +func (x OperatingSystem) VDLWrite(enc vdl.Encoder) error { + if err := enc.WriteValueString(__VDLType_enum_3, x.String()); err != nil { + return err + } + return nil +} + +func (x *OperatingSystem) VDLRead(dec vdl.Decoder) error { + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + if err := x.Set(value); err != nil { + return err + } + } + return nil +} + +// File records the name and contents of a file. +type File struct { + Name string + Contents []byte +} + +func (File) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/build.File"` +}) { +} + +func (x File) VDLIsZero() bool { + if x.Name != "" { + return false + } + if len(x.Contents) != 0 { + return false + } + return true +} + +func (x File) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_4); err != nil { + return err + } + if x.Name != "" { + if err := enc.NextFieldValueString(0, vdl.StringType, x.Name); err != nil { + return err + } + } + if len(x.Contents) != 0 { + if err := enc.NextFieldValueBytes(1, __VDLType_list_5, x.Contents); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *File) VDLRead(dec vdl.Decoder) error { + *x = File{} + if err := dec.StartValue(__VDLType_struct_4); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_4 { + index = __VDLType_struct_4.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.Name = value + } + case 1: + if err := dec.ReadValueBytes(-1, &x.Contents); err != nil { + return err + } + } + } +} + +////////////////////////////////////////////////// +// Interface definitions + +// BuilderClientMethods is the client interface +// containing Builder methods. +// +// Builder describes an interface for building binaries from source. +type BuilderClientMethods interface { + // Build streams sources to the build server, which then attempts to + // build the sources and streams back the compiled binaries. + Build(_ *context.T, arch Architecture, os OperatingSystem, _ ...rpc.CallOpt) (BuilderBuildClientCall, error) + // Describe generates a description for a binary identified by + // the given Object name. + Describe(_ *context.T, name string, _ ...rpc.CallOpt) (binary.Description, error) +} + +// BuilderClientStub adds universal methods to BuilderClientMethods. +type BuilderClientStub interface { + BuilderClientMethods + rpc.UniversalServiceMethods +} + +// BuilderClient returns a client stub for Builder. +func BuilderClient(name string) BuilderClientStub { + return implBuilderClientStub{name} +} + +type implBuilderClientStub struct { + name string +} + +func (c implBuilderClientStub) Build(ctx *context.T, i0 Architecture, i1 OperatingSystem, opts ...rpc.CallOpt) (ocall BuilderBuildClientCall, err error) { + var call rpc.ClientCall + if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "Build", []interface{}{i0, i1}, opts...); err != nil { + return + } + ocall = &implBuilderBuildClientCall{ClientCall: call} + return +} + +func (c implBuilderClientStub) Describe(ctx *context.T, i0 string, opts ...rpc.CallOpt) (o0 binary.Description, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Describe", []interface{}{i0}, []interface{}{&o0}, opts...) + return +} + +// BuilderBuildClientStream is the client stream for Builder.Build. +type BuilderBuildClientStream interface { + // RecvStream returns the receiver side of the Builder.Build client stream. + RecvStream() interface { + // Advance stages an item so that it may be retrieved via Value. Returns + // true iff there is an item to retrieve. Advance must be called before + // Value is called. May block if an item is not available. + Advance() bool + // Value returns the item that was staged by Advance. May panic if Advance + // returned false or was not called. Never blocks. + Value() File + // Err returns any error encountered by Advance. Never blocks. + Err() error + } + // SendStream returns the send side of the Builder.Build client stream. + SendStream() interface { + // Send places the item onto the output stream. Returns errors + // encountered while sending, or if Send is called after Close or + // the stream has been canceled. Blocks if there is no buffer + // space; will unblock when buffer space is available or after + // the stream has been canceled. + Send(item File) error + // Close indicates to the server that no more items will be sent; + // server Recv calls will receive io.EOF after all sent items. + // This is an optional call - e.g. a client might call Close if it + // needs to continue receiving items from the server after it's + // done sending. Returns errors encountered while closing, or if + // Close is called after the stream has been canceled. Like Send, + // blocks if there is no buffer space available. + Close() error + } +} + +// BuilderBuildClientCall represents the call returned from Builder.Build. +type BuilderBuildClientCall interface { + BuilderBuildClientStream + // Finish performs the equivalent of SendStream().Close, then blocks until + // the server is done, and returns the positional return values for the call. + // + // Finish returns immediately if the call has been canceled; depending on the + // timing the output could either be an error signaling cancelation, or the + // valid positional return values from the server. + // + // Calling Finish is mandatory for releasing stream resources, unless the call + // has been canceled or any of the other methods return an error. Finish should + // be called at most once. + Finish() ([]byte, error) +} + +type implBuilderBuildClientCall struct { + rpc.ClientCall + valRecv File + errRecv error +} + +func (c *implBuilderBuildClientCall) RecvStream() interface { + Advance() bool + Value() File + Err() error +} { + return implBuilderBuildClientCallRecv{c} +} + +type implBuilderBuildClientCallRecv struct { + c *implBuilderBuildClientCall +} + +func (c implBuilderBuildClientCallRecv) Advance() bool { + c.c.valRecv = File{} + c.c.errRecv = c.c.Recv(&c.c.valRecv) + return c.c.errRecv == nil +} +func (c implBuilderBuildClientCallRecv) Value() File { + return c.c.valRecv +} +func (c implBuilderBuildClientCallRecv) Err() error { + if c.c.errRecv == io.EOF { + return nil + } + return c.c.errRecv +} +func (c *implBuilderBuildClientCall) SendStream() interface { + Send(item File) error + Close() error +} { + return implBuilderBuildClientCallSend{c} +} + +type implBuilderBuildClientCallSend struct { + c *implBuilderBuildClientCall +} + +func (c implBuilderBuildClientCallSend) Send(item File) error { + return c.c.Send(item) +} +func (c implBuilderBuildClientCallSend) Close() error { + return c.c.CloseSend() +} +func (c *implBuilderBuildClientCall) Finish() (o0 []byte, err error) { + err = c.ClientCall.Finish(&o0) + return +} + +// BuilderServerMethods is the interface a server writer +// implements for Builder. +// +// Builder describes an interface for building binaries from source. +type BuilderServerMethods interface { + // Build streams sources to the build server, which then attempts to + // build the sources and streams back the compiled binaries. + Build(_ *context.T, _ BuilderBuildServerCall, arch Architecture, os OperatingSystem) ([]byte, error) + // Describe generates a description for a binary identified by + // the given Object name. + Describe(_ *context.T, _ rpc.ServerCall, name string) (binary.Description, error) +} + +// BuilderServerStubMethods is the server interface containing +// Builder methods, as expected by rpc.Server. +// The only difference between this interface and BuilderServerMethods +// is the streaming methods. +type BuilderServerStubMethods interface { + // Build streams sources to the build server, which then attempts to + // build the sources and streams back the compiled binaries. + Build(_ *context.T, _ *BuilderBuildServerCallStub, arch Architecture, os OperatingSystem) ([]byte, error) + // Describe generates a description for a binary identified by + // the given Object name. + Describe(_ *context.T, _ rpc.ServerCall, name string) (binary.Description, error) +} + +// BuilderServerStub adds universal methods to BuilderServerStubMethods. +type BuilderServerStub interface { + BuilderServerStubMethods + // Describe the Builder interfaces. + Describe__() []rpc.InterfaceDesc +} + +// BuilderServer returns a server stub for Builder. +// It converts an implementation of BuilderServerMethods into +// an object that may be used by rpc.Server. +func BuilderServer(impl BuilderServerMethods) BuilderServerStub { + stub := implBuilderServerStub{ + impl: impl, + } + // Initialize GlobState; always check the stub itself first, to handle the + // case where the user has the Glob method defined in their VDL source. + if gs := rpc.NewGlobState(stub); gs != nil { + stub.gs = gs + } else if gs := rpc.NewGlobState(impl); gs != nil { + stub.gs = gs + } + return stub +} + +type implBuilderServerStub struct { + impl BuilderServerMethods + gs *rpc.GlobState +} + +func (s implBuilderServerStub) Build(ctx *context.T, call *BuilderBuildServerCallStub, i0 Architecture, i1 OperatingSystem) ([]byte, error) { + return s.impl.Build(ctx, call, i0, i1) +} + +func (s implBuilderServerStub) Describe(ctx *context.T, call rpc.ServerCall, i0 string) (binary.Description, error) { + return s.impl.Describe(ctx, call, i0) +} + +func (s implBuilderServerStub) Globber() *rpc.GlobState { + return s.gs +} + +func (s implBuilderServerStub) Describe__() []rpc.InterfaceDesc { + return []rpc.InterfaceDesc{BuilderDesc} +} + +// BuilderDesc describes the Builder interface. +var BuilderDesc rpc.InterfaceDesc = descBuilder + +// descBuilder hides the desc to keep godoc clean. +var descBuilder = rpc.InterfaceDesc{ + Name: "Builder", + PkgPath: "v.io/v23/services/build", + Doc: "// Builder describes an interface for building binaries from source.", + Methods: []rpc.MethodDesc{ + { + Name: "Build", + Doc: "// Build streams sources to the build server, which then attempts to\n// build the sources and streams back the compiled binaries.", + InArgs: []rpc.ArgDesc{ + {"arch", ``}, // Architecture + {"os", ``}, // OperatingSystem + }, + OutArgs: []rpc.ArgDesc{ + {"", ``}, // []byte + }, + }, + { + Name: "Describe", + Doc: "// Describe generates a description for a binary identified by\n// the given Object name.", + InArgs: []rpc.ArgDesc{ + {"name", ``}, // string + }, + OutArgs: []rpc.ArgDesc{ + {"", ``}, // binary.Description + }, + }, + }, +} + +// BuilderBuildServerStream is the server stream for Builder.Build. +type BuilderBuildServerStream interface { + // RecvStream returns the receiver side of the Builder.Build server stream. + RecvStream() interface { + // Advance stages an item so that it may be retrieved via Value. Returns + // true iff there is an item to retrieve. Advance must be called before + // Value is called. May block if an item is not available. + Advance() bool + // Value returns the item that was staged by Advance. May panic if Advance + // returned false or was not called. Never blocks. + Value() File + // Err returns any error encountered by Advance. Never blocks. + Err() error + } + // SendStream returns the send side of the Builder.Build server stream. + SendStream() interface { + // Send places the item onto the output stream. Returns errors encountered + // while sending. Blocks if there is no buffer space; will unblock when + // buffer space is available. + Send(item File) error + } +} + +// BuilderBuildServerCall represents the context passed to Builder.Build. +type BuilderBuildServerCall interface { + rpc.ServerCall + BuilderBuildServerStream +} + +// BuilderBuildServerCallStub is a wrapper that converts rpc.StreamServerCall into +// a typesafe stub that implements BuilderBuildServerCall. +type BuilderBuildServerCallStub struct { + rpc.StreamServerCall + valRecv File + errRecv error +} + +// Init initializes BuilderBuildServerCallStub from rpc.StreamServerCall. +func (s *BuilderBuildServerCallStub) Init(call rpc.StreamServerCall) { + s.StreamServerCall = call +} + +// RecvStream returns the receiver side of the Builder.Build server stream. +func (s *BuilderBuildServerCallStub) RecvStream() interface { + Advance() bool + Value() File + Err() error +} { + return implBuilderBuildServerCallRecv{s} +} + +type implBuilderBuildServerCallRecv struct { + s *BuilderBuildServerCallStub +} + +func (s implBuilderBuildServerCallRecv) Advance() bool { + s.s.valRecv = File{} + s.s.errRecv = s.s.Recv(&s.s.valRecv) + return s.s.errRecv == nil +} +func (s implBuilderBuildServerCallRecv) Value() File { + return s.s.valRecv +} +func (s implBuilderBuildServerCallRecv) Err() error { + if s.s.errRecv == io.EOF { + return nil + } + return s.s.errRecv +} + +// SendStream returns the send side of the Builder.Build server stream. +func (s *BuilderBuildServerCallStub) SendStream() interface { + Send(item File) error +} { + return implBuilderBuildServerCallSend{s} +} + +type implBuilderBuildServerCallSend struct { + s *BuilderBuildServerCallStub +} + +func (s implBuilderBuildServerCallSend) Send(item File) error { + return s.s.Send(item) +} + +// Hold type definitions in package-level variables, for better performance. +var ( + __VDLType_enum_1 *vdl.Type + __VDLType_enum_2 *vdl.Type + __VDLType_enum_3 *vdl.Type + __VDLType_struct_4 *vdl.Type + __VDLType_list_5 *vdl.Type +) + +var __VDLInitCalled bool + +// __VDLInit performs vdl initialization. It is safe to call multiple times. +// If you have an init ordering issue, just insert the following line verbatim +// into your source files in this package, right after the "package foo" clause: +// +// var _ = __VDLInit() +// +// The purpose of this function is to ensure that vdl initialization occurs in +// the right order, and very early in the init sequence. In particular, vdl +// registration and package variable initialization needs to occur before +// functions like vdl.TypeOf will work properly. +// +// This function returns a dummy value, so that it can be used to initialize the +// first var in the file, to take advantage of Go's defined init order. +func __VDLInit() struct{} { + if __VDLInitCalled { + return struct{}{} + } + __VDLInitCalled = true + + // Register types. + vdl.Register((*Architecture)(nil)) + vdl.Register((*Format)(nil)) + vdl.Register((*OperatingSystem)(nil)) + vdl.Register((*File)(nil)) + + // Initialize type definitions. + __VDLType_enum_1 = vdl.TypeOf((*Architecture)(nil)) + __VDLType_enum_2 = vdl.TypeOf((*Format)(nil)) + __VDLType_enum_3 = vdl.TypeOf((*OperatingSystem)(nil)) + __VDLType_struct_4 = vdl.TypeOf((*File)(nil)).Elem() + __VDLType_list_5 = vdl.TypeOf((*[]byte)(nil)) + + return struct{}{} +} diff --git a/v23/services/build/util.go b/v23/services/build/util.go new file mode 100644 index 000000000..220b494b3 --- /dev/null +++ b/v23/services/build/util.go @@ -0,0 +1,54 @@ +// Copyright 2015 The Vanadium 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 build + +// SetFromGoArch assigns the GOARCH string label to x. In particular, +// it takes care of mapping "386" to "x86" as the former is not a +// valid VDL enum value. +func (x *Architecture) SetFromGoArch(label string) error { + switch label { + case "386": + return x.Set("x86") + default: + return x.Set(label) + } +} + +// SetFromGoOS assigns the GOOS string label to x. +func (x *OperatingSystem) SetFromGoOS(label string) error { + return x.Set(label) +} + +// ToGoArch returns a GOARCH string label for the given Architecture. +// In particular, it takes care of mapping "x86" to "386" as the latter +// is not a valid VDL enum value. +func (arch Architecture) ToGoArch() string { + switch arch { + case ArchitectureAmd64: + return "amd64" + case ArchitectureArm: + return "arm" + case ArchitectureX86: + return "386" + default: + return "unknown" + } +} + +// ToGoOS returns a GOOS string label for the given Architecture. +func (os OperatingSystem) ToGoOS() string { + switch os { + case OperatingSystemDarwin: + return "darwin" + case OperatingSystemLinux: + return "linux" + case OperatingSystemWindows: + return "windows" + case OperatingSystemAndroid: + return "android" + default: + return "unknown" + } +} diff --git a/v23/services/device/device.vdl b/v23/services/device/device.vdl new file mode 100644 index 000000000..d72ecc2ff --- /dev/null +++ b/v23/services/device/device.vdl @@ -0,0 +1,366 @@ +// Copyright 2015 The Vanadium 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 device defines interfaces for managing devices and their +// applications. +package device + +import ( + "time" + + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/services/binary" + "v.io/v23/services/permissions" + "v.io/v23/services/tidyable" +) + +// TODO(caprita): Merge with v23/config and v.io/x/ref/lib/exec/config.go. + +// Config specifies app configuration that overrides what's in the envelope. +type Config map[string]string + +// InstallationState describes the states that an installation can be in at any +// time. +type InstallationState enum { + Active + Uninstalled +} + +// InstanceState describes the states that an instance can be in at any +// time. +type InstanceState enum { + Launching + Running + Dying + NotRunning + Updating + Deleted +} + +// Application can be used to manage applications on a device. This interface +// will be invoked using an object name that identifies the application and its +// installations and instances where applicable. +// +// An application is defined by a title. An application can have multiple +// installations on a device. The installations are grouped under the same +// application, but are otherwise independent of each other. Each installation +// can have zero or more instances (which can be running or not). The instances +// are independent of each other, and do not share state (like local storage). +// Interaction among instances should occur via Vanadium RPC, facilitated by the +// local mounttable. +// +// The device manager supports versioning of applications. Each installation +// maintains a tree of versions, where a version is defined by a specific +// envelope. The tree structure comes from 'previous version' references: each +// version (except the initial installation version) maintains a reference to +// the version that preceded it. The installation maintains a current version +// reference that is used for new instances. Each update operation on the +// installation creates a new version, sets the previous reference of the new +// version to the current version, and then updates the current version to refer +// to the new version. Each revert operation on the installation sets the +// current version to the previous version of the current version. Each +// instance maintains a current version reference that is used to run the +// instance. The initial version of the instance is set to the current version +// of the installation at the time of instantiation. Each update operation on +// the instance updates the instance's current version to the current version of +// the installation. Each revert operation on the instance updates the +// instance's current version to the previous version of the instance's version. +// +// The Application interface methods can be divided based on their intended +// receiver: +// +// 1) Method receiver is an application: +// - Install() +// +// 2) Method receiver is an application installation: +// - Instantiate() +// - Uninstall() +// +// 3) Method receiver is an application instance: +// - Run() +// - Kill() +// - Delete() +// +// 4) Method receiver is an application installation or instance: +// - Update() +// - Revert() +// +// The following methods complement one another: +// - Install() and Uninstall() +// - Instantiate() and Delete() +// - Run() and Kill() +// - Update() and Revert() +// +// +// +// Examples: +// +// Install Google Maps on the device. +// device/apps.Install("/google.com/appstore/maps", nil, nil) --> "google maps/0" +// +// Create and start an instance of the previously installed maps application +// installation. +// device/apps/google maps/0.Instantiate() --> { "0" } +// device/apps/google maps/0/0.Run() +// +// Create and start a second instance of the previously installed maps +// application installation. +// device/apps/google maps/0.Instantiate() --> { "1" } +// device/apps/google maps/0/1.Run() +// +// Kill and delete the first instance previously started. +// device/apps/google maps/0/0.Kill() +// device/apps/google maps/0/0.Delete() +// +// Install a second Google Maps installation. +// device/apps.Install("/google.com/appstore/maps", nil, nil) --> "google maps/1" +// +// Update the second maps installation to the latest version available. +// device/apps/google maps/1.Update() +// +// Update the first maps installation to a specific version. +// device/apps/google maps/0.UpdateTo("/google.com/appstore/beta/maps") +// +// Finally, an application installation instance can be in one of three abstract +// states: 1) "does not exist/deleted", 2) "running", or 3) "not-running". The +// interface methods transition between these abstract states using the +// following state machine: +// +// apply(Instantiate(), "does not exist") = "not-running" +// apply(Run(), "not-running") = "running" +// apply(Kill(), "running") = "not-running" +// apply(Delete(), "not-running") = "deleted" +type Application interface { + // Object provides GetPermissions/SetPermissions methods to read/modify + // AccessLists for the Application methods. After a device has been + // claimed, only the claimant will be able to modify the AccessLists for + // the device. + permissions.Object + + // TODO(caprita): Rather than overriding config and package piecemeal, + // consider providing an envelope override during install. + + // Install installs the application identified by the first argument and + // returns an object name suffix that identifies the new installation. + // + // The name argument should be an object name for an application + // envelope. The service it identifies must implement + // repository.Application, and is expected to return either the + // requested version (if the object name encodes a specific version), or + // otherwise the latest available version, as appropriate. This object + // name will be used by default by the Update method, as a source for + // updated application envelopes (can be overriden by setting + // AppOriginConfigKey in the config). + // + // The config argument specifies config settings that will take + // precedence over those present in the application envelope. + // + // The packages argument specifies packages to be installed in addition + // to those specified in the envelope. If a package in the envelope has + // the same key, the package in the packages argument takes precedence. + // + // The returned suffix, when appended to the name used to reach the + // receiver for Install, can be used to control the installation object. + // The suffix will contain the title of the application as a prefix, + // which can then be used to control all the installations of the given + // application. + // TODO(rjkroege): Use customized labels. + Install(name string, config Config, packages application.Packages) (string | error) {access.Write} + + // Uninstall uninstalls an application installation. + // The installation must be in state Active. + Uninstall() error {access.Admin} + + // Instantiate creates an instance of an application installation. + // The installation must be in state Active. + // + // The server sends the application instance's Public Key on the stream. + // When the client receives the Public Key it must send Blessings back + // to the server. When the instance is created, the server returns the + // instance name to the client. + // + // Client Server + // "object".Instantiate() --> + // <-- InstancePublicKey + // AppBlessings --> + // <-- return InstanceName + Instantiate() stream (string | error) {access.Read} + + // TODO(caprita): Add a new method Shutdown for Device instead of using + // Delete. + + // Delete deletes an instance. Once deleted, the instance cannot be + // revived. + // The instance must be in state NotRunning. + // + // If called against a Device, causes the Device to shut itself down. + Delete() error {access.Admin} + + // Run begins execution of an application instance. + // The instance must be in state NotRunning. + Run() error {access.Write} + + // Kill attempts a clean shutdown an of application instance. + // The instance must be in state Running. + // + // If the deadline is non-zero and the instance in question is still + // running after the given deadline, shutdown of the instance is + // enforced. + // + // If called against a Device, causes the Device to stop itself (which + // may or may not result in a restart depending on the device manager + // setup). + Kill(deadline time.Duration) error {access.Write} + + // Update updates an application installation's version to a new version + // created from the envelope at the object name provided during Install. + // If the new application envelope contains a different application + // title, the update does not occur, and an error is returned. The + // installation must be in state Active. + // + // Update updates an application instance's version to the current + // installation version. The instance must be in state NotRunning. + Update() error {access.Admin} + // TODO(caprita): Decide if we keep this in v0.1. If we do, we may want + // to use it over the origin override mechanism in the config, to + // specify a new origin for the app installation. + + // UpdateTo updates the application installation(s) to the application + // specified by the object name argument. If the new application + // envelope contains a different application title, the update does not + // occur, and an error is returned. + // The installation must be in state Active. + UpdateTo(name string) error {access.Admin} + + // Revert reverts an application installation's version to the previous + // version of its current version. The installation must be in state + // Active. + // + // Revert reverts an application instance's version to the previous + // version of its current version. The instance must be in state + // NotRunning. + Revert() error {access.Admin} + + // Debug returns debug information about the application installation or + // instance. This is generally highly implementation-specific, and + // presented in an unstructured form. No guarantees are given about the + // stability of the format, and parsing it programmatically is + // specifically discouraged. + Debug() (string | error) {access.Debug} + + // Status returns structured information about the application + // installation or instance. + Status() (Status | error) {access.Read} +} + +// Status is returned by the Application Status method. +type Status union { + Instance InstanceStatus + Installation InstallationStatus + Device DeviceStatus +} + +// InstallationStatus specifies the Status returned by the Application Status +// method for installation objects. +type InstallationStatus struct { + State InstallationState + Version string +} + +// InstanceStatus specifies the Status returned by the Application Status method +// for instance objects. +type InstanceStatus struct { + State InstanceState + Version string +} + +// DeviceStatus specifies the Status returned by the Application Status method +// for the device service object. +type DeviceStatus struct { + State InstanceState + Version string +} + +// BlessServerMessage is the data type that is streamed from the server to the +// client during an Instantiate method call. +// This is a union to enable backward compatible changes. +type BlessServerMessage union { + // The public key of the instance being blessed. The client must return + // blessings for this key. + InstancePublicKey []byte +} + +// BlessClientMessage is the data type that is streamed from the client to the +// server during a Instantiate method call. +// This is a union to enable backward compatible changes. +type BlessClientMessage union { + // Blessings for the application instance. + AppBlessings security.WireBlessings +} + +// Description enumerates the profiles that a Device supports. +type Description struct { + // Profiles is a set of names of supported profiles. Each name can + // either be an object name that resolves to a Profile, or can be the + // profile's label, e.g.: + // "profiles/google/cluster/diskfull" + // "linux-media" + // + // Profiles for devices can be provided by hand, but they can also be + // automatically derived by examining the device. + Profiles set[string] +} + +// Association is a tuple containing an association between a Vanadium +// identity and a system account name. +type Association struct { + IdentityName string + AccountName string +} + +// Claimable represents an uninitialized device with no owner +// (i.e., a device that has no blessings). +// +// Claim is used to claim ownership by blessing the device's private key. +// Devices that have provided a pairing token to the claimer through an +// out-of-band communication channel (eg: display/email) would expect this +// pairing token to be replayed by the claimer. +// +// Once claimed, the device will export the "Device" interface and all methods +// will be restricted to the claimer. +// +// The blessings that the device is to be claimed with is provided +// via the ipc.Granter option in Go. +type Claimable interface { + Claim(pairingToken string) error {access.Admin} +} + +// Device can be used to manage a device remotely using an object name that +// identifies it. +type Device interface { + // Each method of the Application interface invoked at the device + // level applies to all applications installed on the device (and + // their installations and instances where applicable). + Application + // The device manager is tidyable. + tidyable.Tidyable + // Describe generates a description of the device. + Describe() (Description | error) {access.Admin} + // IsRunnable checks if the device can execute the given binary. + IsRunnable(description binary.Description) (bool | error) {access.Admin} + // Reset resets the device. If the deadline is non-zero and the device + // in question is still running after the given deadline expired, + // reset of the device is enforced. + Reset(deadline time.Duration) error {access.Admin} + // AssociateAccount associates a local system account name with the provided + // Vanadium identities. It replaces the existing association if one already exists for that + // identity. Setting an AccountName to "" removes the association for each + // listed identity. + AssociateAccount(identityNames []string, accountName string) error {access.Admin} + // ListAssociations returns all of the associations between Vanadium identities + // and system names. + ListAssociations() ([]Association | error) {access.Admin} +} diff --git a/v23/services/device/device.vdl.go b/v23/services/device/device.vdl.go new file mode 100644 index 000000000..b17d63760 --- /dev/null +++ b/v23/services/device/device.vdl.go @@ -0,0 +1,2834 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated by the vanadium vdl tool. +// Package: device + +// Package device defines interfaces for managing devices and their +// applications. +package device + +import ( + "fmt" + "io" + "time" + "v.io/v23" + "v.io/v23/context" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/services/binary" + "v.io/v23/services/permissions" + "v.io/v23/services/tidyable" + "v.io/v23/vdl" + _ "v.io/v23/vdlroot/time" +) + +var _ = __VDLInit() // Must be first; see __VDLInit comments for details. + +////////////////////////////////////////////////// +// Type definitions + +// Config specifies app configuration that overrides what's in the envelope. +type Config map[string]string + +func (Config) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/device.Config"` +}) { +} + +func (x Config) VDLIsZero() bool { + return len(x) == 0 +} + +func (x Config) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_map_1); err != nil { + return err + } + if err := enc.SetLenHint(len(x)); err != nil { + return err + } + for key, elem := range x { + if err := enc.NextEntryValueString(vdl.StringType, key); err != nil { + return err + } + if err := enc.WriteValueString(vdl.StringType, elem); err != nil { + return err + } + } + if err := enc.NextEntry(true); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *Config) VDLRead(dec vdl.Decoder) error { + if err := dec.StartValue(__VDLType_map_1); err != nil { + return err + } + var tmpMap Config + if len := dec.LenHint(); len > 0 { + tmpMap = make(Config, len) + } + for { + switch done, key, err := dec.NextEntryValueString(); { + case err != nil: + return err + case done: + *x = tmpMap + return dec.FinishValue() + default: + var elem string + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + elem = value + } + if tmpMap == nil { + tmpMap = make(Config) + } + tmpMap[key] = elem + } + } +} + +// InstallationState describes the states that an installation can be in at any +// time. +type InstallationState int + +const ( + InstallationStateActive InstallationState = iota + InstallationStateUninstalled +) + +// InstallationStateAll holds all labels for InstallationState. +var InstallationStateAll = [...]InstallationState{InstallationStateActive, InstallationStateUninstalled} + +// InstallationStateFromString creates a InstallationState from a string label. +func InstallationStateFromString(label string) (x InstallationState, err error) { + err = x.Set(label) + return +} + +// Set assigns label to x. +func (x *InstallationState) Set(label string) error { + switch label { + case "Active", "active": + *x = InstallationStateActive + return nil + case "Uninstalled", "uninstalled": + *x = InstallationStateUninstalled + return nil + } + *x = -1 + return fmt.Errorf("unknown label %q in device.InstallationState", label) +} + +// String returns the string label of x. +func (x InstallationState) String() string { + switch x { + case InstallationStateActive: + return "Active" + case InstallationStateUninstalled: + return "Uninstalled" + } + return "" +} + +func (InstallationState) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/device.InstallationState"` + Enum struct{ Active, Uninstalled string } +}) { +} + +func (x InstallationState) VDLIsZero() bool { + return x == InstallationStateActive +} + +func (x InstallationState) VDLWrite(enc vdl.Encoder) error { + if err := enc.WriteValueString(__VDLType_enum_2, x.String()); err != nil { + return err + } + return nil +} + +func (x *InstallationState) VDLRead(dec vdl.Decoder) error { + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + if err := x.Set(value); err != nil { + return err + } + } + return nil +} + +// InstanceState describes the states that an instance can be in at any +// time. +type InstanceState int + +const ( + InstanceStateLaunching InstanceState = iota + InstanceStateRunning + InstanceStateDying + InstanceStateNotRunning + InstanceStateUpdating + InstanceStateDeleted +) + +// InstanceStateAll holds all labels for InstanceState. +var InstanceStateAll = [...]InstanceState{InstanceStateLaunching, InstanceStateRunning, InstanceStateDying, InstanceStateNotRunning, InstanceStateUpdating, InstanceStateDeleted} + +// InstanceStateFromString creates a InstanceState from a string label. +func InstanceStateFromString(label string) (x InstanceState, err error) { + err = x.Set(label) + return +} + +// Set assigns label to x. +func (x *InstanceState) Set(label string) error { + switch label { + case "Launching", "launching": + *x = InstanceStateLaunching + return nil + case "Running", "running": + *x = InstanceStateRunning + return nil + case "Dying", "dying": + *x = InstanceStateDying + return nil + case "NotRunning", "notrunning": + *x = InstanceStateNotRunning + return nil + case "Updating", "updating": + *x = InstanceStateUpdating + return nil + case "Deleted", "deleted": + *x = InstanceStateDeleted + return nil + } + *x = -1 + return fmt.Errorf("unknown label %q in device.InstanceState", label) +} + +// String returns the string label of x. +func (x InstanceState) String() string { + switch x { + case InstanceStateLaunching: + return "Launching" + case InstanceStateRunning: + return "Running" + case InstanceStateDying: + return "Dying" + case InstanceStateNotRunning: + return "NotRunning" + case InstanceStateUpdating: + return "Updating" + case InstanceStateDeleted: + return "Deleted" + } + return "" +} + +func (InstanceState) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/device.InstanceState"` + Enum struct{ Launching, Running, Dying, NotRunning, Updating, Deleted string } +}) { +} + +func (x InstanceState) VDLIsZero() bool { + return x == InstanceStateLaunching +} + +func (x InstanceState) VDLWrite(enc vdl.Encoder) error { + if err := enc.WriteValueString(__VDLType_enum_3, x.String()); err != nil { + return err + } + return nil +} + +func (x *InstanceState) VDLRead(dec vdl.Decoder) error { + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + if err := x.Set(value); err != nil { + return err + } + } + return nil +} + +// InstanceStatus specifies the Status returned by the Application Status method +// for instance objects. +type InstanceStatus struct { + State InstanceState + Version string +} + +func (InstanceStatus) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/device.InstanceStatus"` +}) { +} + +func (x InstanceStatus) VDLIsZero() bool { + return x == InstanceStatus{} +} + +func (x InstanceStatus) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_4); err != nil { + return err + } + if x.State != InstanceStateLaunching { + if err := enc.NextFieldValueString(0, __VDLType_enum_3, x.State.String()); err != nil { + return err + } + } + if x.Version != "" { + if err := enc.NextFieldValueString(1, vdl.StringType, x.Version); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *InstanceStatus) VDLRead(dec vdl.Decoder) error { + *x = InstanceStatus{} + if err := dec.StartValue(__VDLType_struct_4); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_4 { + index = __VDLType_struct_4.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + if err := x.State.Set(value); err != nil { + return err + } + } + case 1: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.Version = value + } + } + } +} + +// InstallationStatus specifies the Status returned by the Application Status +// method for installation objects. +type InstallationStatus struct { + State InstallationState + Version string +} + +func (InstallationStatus) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/device.InstallationStatus"` +}) { +} + +func (x InstallationStatus) VDLIsZero() bool { + return x == InstallationStatus{} +} + +func (x InstallationStatus) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_5); err != nil { + return err + } + if x.State != InstallationStateActive { + if err := enc.NextFieldValueString(0, __VDLType_enum_2, x.State.String()); err != nil { + return err + } + } + if x.Version != "" { + if err := enc.NextFieldValueString(1, vdl.StringType, x.Version); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *InstallationStatus) VDLRead(dec vdl.Decoder) error { + *x = InstallationStatus{} + if err := dec.StartValue(__VDLType_struct_5); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_5 { + index = __VDLType_struct_5.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + if err := x.State.Set(value); err != nil { + return err + } + } + case 1: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.Version = value + } + } + } +} + +// DeviceStatus specifies the Status returned by the Application Status method +// for the device service object. +type DeviceStatus struct { + State InstanceState + Version string +} + +func (DeviceStatus) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/device.DeviceStatus"` +}) { +} + +func (x DeviceStatus) VDLIsZero() bool { + return x == DeviceStatus{} +} + +func (x DeviceStatus) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_6); err != nil { + return err + } + if x.State != InstanceStateLaunching { + if err := enc.NextFieldValueString(0, __VDLType_enum_3, x.State.String()); err != nil { + return err + } + } + if x.Version != "" { + if err := enc.NextFieldValueString(1, vdl.StringType, x.Version); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *DeviceStatus) VDLRead(dec vdl.Decoder) error { + *x = DeviceStatus{} + if err := dec.StartValue(__VDLType_struct_6); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_6 { + index = __VDLType_struct_6.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + if err := x.State.Set(value); err != nil { + return err + } + } + case 1: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.Version = value + } + } + } +} + +type ( + // Status represents any single field of the Status union type. + // + // Status is returned by the Application Status method. + Status interface { + // Index returns the field index. + Index() int + // Interface returns the field value as an interface. + Interface() interface{} + // Name returns the field name. + Name() string + // VDLReflect describes the Status union type. + VDLReflect(__StatusReflect) + VDLIsZero() bool + VDLWrite(vdl.Encoder) error + } + // StatusInstance represents field Instance of the Status union type. + StatusInstance struct{ Value InstanceStatus } + // StatusInstallation represents field Installation of the Status union type. + StatusInstallation struct{ Value InstallationStatus } + // StatusDevice represents field Device of the Status union type. + StatusDevice struct{ Value DeviceStatus } + // __StatusReflect describes the Status union type. + __StatusReflect struct { + Name string `vdl:"v.io/v23/services/device.Status"` + Type Status + Union struct { + Instance StatusInstance + Installation StatusInstallation + Device StatusDevice + } + } +) + +func (x StatusInstance) Index() int { return 0 } +func (x StatusInstance) Interface() interface{} { return x.Value } +func (x StatusInstance) Name() string { return "Instance" } +func (x StatusInstance) VDLReflect(__StatusReflect) {} + +func (x StatusInstallation) Index() int { return 1 } +func (x StatusInstallation) Interface() interface{} { return x.Value } +func (x StatusInstallation) Name() string { return "Installation" } +func (x StatusInstallation) VDLReflect(__StatusReflect) {} + +func (x StatusDevice) Index() int { return 2 } +func (x StatusDevice) Interface() interface{} { return x.Value } +func (x StatusDevice) Name() string { return "Device" } +func (x StatusDevice) VDLReflect(__StatusReflect) {} + +func (x StatusInstance) VDLIsZero() bool { + return x.Value == InstanceStatus{} +} + +func (x StatusInstallation) VDLIsZero() bool { + return false +} + +func (x StatusDevice) VDLIsZero() bool { + return false +} + +func (x StatusInstance) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_union_7); err != nil { + return err + } + if err := enc.NextField(0); err != nil { + return err + } + if err := x.Value.VDLWrite(enc); err != nil { + return err + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func (x StatusInstallation) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_union_7); err != nil { + return err + } + if err := enc.NextField(1); err != nil { + return err + } + if err := x.Value.VDLWrite(enc); err != nil { + return err + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func (x StatusDevice) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_union_7); err != nil { + return err + } + if err := enc.NextField(2); err != nil { + return err + } + if err := x.Value.VDLWrite(enc); err != nil { + return err + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func VDLReadStatus(dec vdl.Decoder, x *Status) error { + if err := dec.StartValue(__VDLType_union_7); err != nil { + return err + } + decType := dec.Type() + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return fmt.Errorf("missing field in union %T, from %v", x, decType) + } + if decType != __VDLType_union_7 { + name := decType.Field(index).Name + index = __VDLType_union_7.FieldIndexByName(name) + if index == -1 { + return fmt.Errorf("field %q not in union %T, from %v", name, x, decType) + } + } + switch index { + case 0: + var field StatusInstance + if err := field.Value.VDLRead(dec); err != nil { + return err + } + *x = field + case 1: + var field StatusInstallation + if err := field.Value.VDLRead(dec); err != nil { + return err + } + *x = field + case 2: + var field StatusDevice + if err := field.Value.VDLRead(dec); err != nil { + return err + } + *x = field + } + switch index, err := dec.NextField(); { + case err != nil: + return err + case index != -1: + return fmt.Errorf("extra field %d in union %T, from %v", index, x, dec.Type()) + } + return dec.FinishValue() +} + +type ( + // BlessServerMessage represents any single field of the BlessServerMessage union type. + // + // BlessServerMessage is the data type that is streamed from the server to the + // client during an Instantiate method call. + // This is a union to enable backward compatible changes. + BlessServerMessage interface { + // Index returns the field index. + Index() int + // Interface returns the field value as an interface. + Interface() interface{} + // Name returns the field name. + Name() string + // VDLReflect describes the BlessServerMessage union type. + VDLReflect(__BlessServerMessageReflect) + VDLIsZero() bool + VDLWrite(vdl.Encoder) error + } + // BlessServerMessageInstancePublicKey represents field InstancePublicKey of the BlessServerMessage union type. + // + // The public key of the instance being blessed. The client must return + // blessings for this key. + BlessServerMessageInstancePublicKey struct{ Value []byte } + // __BlessServerMessageReflect describes the BlessServerMessage union type. + __BlessServerMessageReflect struct { + Name string `vdl:"v.io/v23/services/device.BlessServerMessage"` + Type BlessServerMessage + Union struct { + InstancePublicKey BlessServerMessageInstancePublicKey + } + } +) + +func (x BlessServerMessageInstancePublicKey) Index() int { return 0 } +func (x BlessServerMessageInstancePublicKey) Interface() interface{} { return x.Value } +func (x BlessServerMessageInstancePublicKey) Name() string { return "InstancePublicKey" } +func (x BlessServerMessageInstancePublicKey) VDLReflect(__BlessServerMessageReflect) {} + +func (x BlessServerMessageInstancePublicKey) VDLIsZero() bool { + return len(x.Value) == 0 +} + +func (x BlessServerMessageInstancePublicKey) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_union_9); err != nil { + return err + } + if err := enc.NextFieldValueBytes(0, __VDLType_list_8, x.Value); err != nil { + return err + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func VDLReadBlessServerMessage(dec vdl.Decoder, x *BlessServerMessage) error { + if err := dec.StartValue(__VDLType_union_9); err != nil { + return err + } + decType := dec.Type() + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return fmt.Errorf("missing field in union %T, from %v", x, decType) + } + if decType != __VDLType_union_9 { + name := decType.Field(index).Name + index = __VDLType_union_9.FieldIndexByName(name) + if index == -1 { + return fmt.Errorf("field %q not in union %T, from %v", name, x, decType) + } + } + switch index { + case 0: + var field BlessServerMessageInstancePublicKey + if err := dec.ReadValueBytes(-1, &field.Value); err != nil { + return err + } + *x = field + } + switch index, err := dec.NextField(); { + case err != nil: + return err + case index != -1: + return fmt.Errorf("extra field %d in union %T, from %v", index, x, dec.Type()) + } + return dec.FinishValue() +} + +type ( + // BlessClientMessage represents any single field of the BlessClientMessage union type. + // + // BlessClientMessage is the data type that is streamed from the client to the + // server during a Instantiate method call. + // This is a union to enable backward compatible changes. + BlessClientMessage interface { + // Index returns the field index. + Index() int + // Interface returns the field value as an interface. + Interface() interface{} + // Name returns the field name. + Name() string + // VDLReflect describes the BlessClientMessage union type. + VDLReflect(__BlessClientMessageReflect) + VDLIsZero() bool + VDLWrite(vdl.Encoder) error + } + // BlessClientMessageAppBlessings represents field AppBlessings of the BlessClientMessage union type. + // + // Blessings for the application instance. + BlessClientMessageAppBlessings struct{ Value security.Blessings } + // __BlessClientMessageReflect describes the BlessClientMessage union type. + __BlessClientMessageReflect struct { + Name string `vdl:"v.io/v23/services/device.BlessClientMessage"` + Type BlessClientMessage + Union struct { + AppBlessings BlessClientMessageAppBlessings + } + } +) + +func (x BlessClientMessageAppBlessings) Index() int { return 0 } +func (x BlessClientMessageAppBlessings) Interface() interface{} { return x.Value } +func (x BlessClientMessageAppBlessings) Name() string { return "AppBlessings" } +func (x BlessClientMessageAppBlessings) VDLReflect(__BlessClientMessageReflect) {} + +func (x BlessClientMessageAppBlessings) VDLIsZero() bool { + return x.Value.IsZero() +} + +func (x BlessClientMessageAppBlessings) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_union_11); err != nil { + return err + } + if err := enc.NextField(0); err != nil { + return err + } + var wire security.WireBlessings + if err := security.WireBlessingsFromNative(&wire, x.Value); err != nil { + return err + } + if err := wire.VDLWrite(enc); err != nil { + return err + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func VDLReadBlessClientMessage(dec vdl.Decoder, x *BlessClientMessage) error { + if err := dec.StartValue(__VDLType_union_11); err != nil { + return err + } + decType := dec.Type() + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return fmt.Errorf("missing field in union %T, from %v", x, decType) + } + if decType != __VDLType_union_11 { + name := decType.Field(index).Name + index = __VDLType_union_11.FieldIndexByName(name) + if index == -1 { + return fmt.Errorf("field %q not in union %T, from %v", name, x, decType) + } + } + switch index { + case 0: + var field BlessClientMessageAppBlessings + var wire security.WireBlessings + if err := wire.VDLRead(dec); err != nil { + return err + } + if err := security.WireBlessingsToNative(wire, &field.Value); err != nil { + return err + } + *x = field + } + switch index, err := dec.NextField(); { + case err != nil: + return err + case index != -1: + return fmt.Errorf("extra field %d in union %T, from %v", index, x, dec.Type()) + } + return dec.FinishValue() +} + +// Description enumerates the profiles that a Device supports. +type Description struct { + // Profiles is a set of names of supported profiles. Each name can + // either be an object name that resolves to a Profile, or can be the + // profile's label, e.g.: + // "profiles/google/cluster/diskfull" + // "linux-media" + // + // Profiles for devices can be provided by hand, but they can also be + // automatically derived by examining the device. + Profiles map[string]struct{} +} + +func (Description) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/device.Description"` +}) { +} + +func (x Description) VDLIsZero() bool { + if len(x.Profiles) != 0 { + return false + } + return true +} + +func (x Description) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_12); err != nil { + return err + } + if len(x.Profiles) != 0 { + if err := enc.NextField(0); err != nil { + return err + } + if err := __VDLWriteAnon_set_1(enc, x.Profiles); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func __VDLWriteAnon_set_1(enc vdl.Encoder, x map[string]struct{}) error { + if err := enc.StartValue(__VDLType_set_13); err != nil { + return err + } + if err := enc.SetLenHint(len(x)); err != nil { + return err + } + for key := range x { + if err := enc.NextEntryValueString(vdl.StringType, key); err != nil { + return err + } + } + if err := enc.NextEntry(true); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *Description) VDLRead(dec vdl.Decoder) error { + *x = Description{} + if err := dec.StartValue(__VDLType_struct_12); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_12 { + index = __VDLType_struct_12.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + if err := __VDLReadAnon_set_1(dec, &x.Profiles); err != nil { + return err + } + } + } +} + +func __VDLReadAnon_set_1(dec vdl.Decoder, x *map[string]struct{}) error { + if err := dec.StartValue(__VDLType_set_13); err != nil { + return err + } + var tmpMap map[string]struct{} + if len := dec.LenHint(); len > 0 { + tmpMap = make(map[string]struct{}, len) + } + for { + switch done, key, err := dec.NextEntryValueString(); { + case err != nil: + return err + case done: + *x = tmpMap + return dec.FinishValue() + default: + if tmpMap == nil { + tmpMap = make(map[string]struct{}) + } + tmpMap[key] = struct{}{} + } + } +} + +// Association is a tuple containing an association between a Vanadium +// identity and a system account name. +type Association struct { + IdentityName string + AccountName string +} + +func (Association) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/device.Association"` +}) { +} + +func (x Association) VDLIsZero() bool { + return x == Association{} +} + +func (x Association) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_14); err != nil { + return err + } + if x.IdentityName != "" { + if err := enc.NextFieldValueString(0, vdl.StringType, x.IdentityName); err != nil { + return err + } + } + if x.AccountName != "" { + if err := enc.NextFieldValueString(1, vdl.StringType, x.AccountName); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *Association) VDLRead(dec vdl.Decoder) error { + *x = Association{} + if err := dec.StartValue(__VDLType_struct_14); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_14 { + index = __VDLType_struct_14.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.IdentityName = value + } + case 1: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.AccountName = value + } + } + } +} + +////////////////////////////////////////////////// +// Interface definitions + +// ApplicationClientMethods is the client interface +// containing Application methods. +// +// Application can be used to manage applications on a device. This interface +// will be invoked using an object name that identifies the application and its +// installations and instances where applicable. +// +// An application is defined by a title. An application can have multiple +// installations on a device. The installations are grouped under the same +// application, but are otherwise independent of each other. Each installation +// can have zero or more instances (which can be running or not). The instances +// are independent of each other, and do not share state (like local storage). +// Interaction among instances should occur via Vanadium RPC, facilitated by the +// local mounttable. +// +// The device manager supports versioning of applications. Each installation +// maintains a tree of versions, where a version is defined by a specific +// envelope. The tree structure comes from 'previous version' references: each +// version (except the initial installation version) maintains a reference to +// the version that preceded it. The installation maintains a current version +// reference that is used for new instances. Each update operation on the +// installation creates a new version, sets the previous reference of the new +// version to the current version, and then updates the current version to refer +// to the new version. Each revert operation on the installation sets the +// current version to the previous version of the current version. Each +// instance maintains a current version reference that is used to run the +// instance. The initial version of the instance is set to the current version +// of the installation at the time of instantiation. Each update operation on +// the instance updates the instance's current version to the current version of +// the installation. Each revert operation on the instance updates the +// instance's current version to the previous version of the instance's version. +// +// The Application interface methods can be divided based on their intended +// receiver: +// +// 1) Method receiver is an application: +// - Install() +// +// 2) Method receiver is an application installation: +// - Instantiate() +// - Uninstall() +// +// 3) Method receiver is an application instance: +// - Run() +// - Kill() +// - Delete() +// +// 4) Method receiver is an application installation or instance: +// - Update() +// - Revert() +// +// The following methods complement one another: +// - Install() and Uninstall() +// - Instantiate() and Delete() +// - Run() and Kill() +// - Update() and Revert() +// +// +// +// Examples: +// +// Install Google Maps on the device. +// device/apps.Install("/google.com/appstore/maps", nil, nil) --> "google maps/0" +// +// Create and start an instance of the previously installed maps application +// installation. +// device/apps/google maps/0.Instantiate() --> { "0" } +// device/apps/google maps/0/0.Run() +// +// Create and start a second instance of the previously installed maps +// application installation. +// device/apps/google maps/0.Instantiate() --> { "1" } +// device/apps/google maps/0/1.Run() +// +// Kill and delete the first instance previously started. +// device/apps/google maps/0/0.Kill() +// device/apps/google maps/0/0.Delete() +// +// Install a second Google Maps installation. +// device/apps.Install("/google.com/appstore/maps", nil, nil) --> "google maps/1" +// +// Update the second maps installation to the latest version available. +// device/apps/google maps/1.Update() +// +// Update the first maps installation to a specific version. +// device/apps/google maps/0.UpdateTo("/google.com/appstore/beta/maps") +// +// Finally, an application installation instance can be in one of three abstract +// states: 1) "does not exist/deleted", 2) "running", or 3) "not-running". The +// interface methods transition between these abstract states using the +// following state machine: +// +// apply(Instantiate(), "does not exist") = "not-running" +// apply(Run(), "not-running") = "running" +// apply(Kill(), "running") = "not-running" +// apply(Delete(), "not-running") = "deleted" +type ApplicationClientMethods interface { + // Object provides access control for Vanadium objects. + // + // Vanadium services implementing dynamic access control would typically embed + // this interface and tag additional methods defined by the service with one of + // Admin, Read, Write, Resolve etc. For example, the VDL definition of the + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // import "v.io/v23/services/permissions" + // + // type MyObject interface { + // permissions.Object + // MyRead() (string, error) {access.Read} + // MyWrite(string) error {access.Write} + // } + // + // If the set of pre-defined tags is insufficient, services may define their + // own tag type and annotate all methods with this new type. + // + // Instead of embedding this Object interface, define SetPermissions and + // GetPermissions in their own interface. Authorization policies will typically + // respect annotations of a single type. For example, the VDL definition of an + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // + // type MyTag string + // + // const ( + // Blue = MyTag("Blue") + // Red = MyTag("Red") + // ) + // + // type MyObject interface { + // MyMethod() (string, error) {Blue} + // + // // Allow clients to change access via the access.Object interface: + // SetPermissions(perms access.Permissions, version string) error {Red} + // GetPermissions() (perms access.Permissions, version string, err error) {Blue} + // } + permissions.ObjectClientMethods + // Install installs the application identified by the first argument and + // returns an object name suffix that identifies the new installation. + // + // The name argument should be an object name for an application + // envelope. The service it identifies must implement + // repository.Application, and is expected to return either the + // requested version (if the object name encodes a specific version), or + // otherwise the latest available version, as appropriate. This object + // name will be used by default by the Update method, as a source for + // updated application envelopes (can be overriden by setting + // AppOriginConfigKey in the config). + // + // The config argument specifies config settings that will take + // precedence over those present in the application envelope. + // + // The packages argument specifies packages to be installed in addition + // to those specified in the envelope. If a package in the envelope has + // the same key, the package in the packages argument takes precedence. + // + // The returned suffix, when appended to the name used to reach the + // receiver for Install, can be used to control the installation object. + // The suffix will contain the title of the application as a prefix, + // which can then be used to control all the installations of the given + // application. + // TODO(rjkroege): Use customized labels. + Install(_ *context.T, name string, config Config, packages application.Packages, _ ...rpc.CallOpt) (string, error) + // Uninstall uninstalls an application installation. + // The installation must be in state Active. + Uninstall(*context.T, ...rpc.CallOpt) error + // Instantiate creates an instance of an application installation. + // The installation must be in state Active. + // + // The server sends the application instance's Public Key on the stream. + // When the client receives the Public Key it must send Blessings back + // to the server. When the instance is created, the server returns the + // instance name to the client. + // + // Client Server + // "object".Instantiate() --> + // <-- InstancePublicKey + // AppBlessings --> + // <-- return InstanceName + Instantiate(*context.T, ...rpc.CallOpt) (ApplicationInstantiateClientCall, error) + // Delete deletes an instance. Once deleted, the instance cannot be + // revived. + // The instance must be in state NotRunning. + // + // If called against a Device, causes the Device to shut itself down. + Delete(*context.T, ...rpc.CallOpt) error + // Run begins execution of an application instance. + // The instance must be in state NotRunning. + Run(*context.T, ...rpc.CallOpt) error + // Kill attempts a clean shutdown an of application instance. + // The instance must be in state Running. + // + // If the deadline is non-zero and the instance in question is still + // running after the given deadline, shutdown of the instance is + // enforced. + // + // If called against a Device, causes the Device to stop itself (which + // may or may not result in a restart depending on the device manager + // setup). + Kill(_ *context.T, deadline time.Duration, _ ...rpc.CallOpt) error + // Update updates an application installation's version to a new version + // created from the envelope at the object name provided during Install. + // If the new application envelope contains a different application + // title, the update does not occur, and an error is returned. The + // installation must be in state Active. + // + // Update updates an application instance's version to the current + // installation version. The instance must be in state NotRunning. + Update(*context.T, ...rpc.CallOpt) error + // UpdateTo updates the application installation(s) to the application + // specified by the object name argument. If the new application + // envelope contains a different application title, the update does not + // occur, and an error is returned. + // The installation must be in state Active. + UpdateTo(_ *context.T, name string, _ ...rpc.CallOpt) error + // Revert reverts an application installation's version to the previous + // version of its current version. The installation must be in state + // Active. + // + // Revert reverts an application instance's version to the previous + // version of its current version. The instance must be in state + // NotRunning. + Revert(*context.T, ...rpc.CallOpt) error + // Debug returns debug information about the application installation or + // instance. This is generally highly implementation-specific, and + // presented in an unstructured form. No guarantees are given about the + // stability of the format, and parsing it programmatically is + // specifically discouraged. + Debug(*context.T, ...rpc.CallOpt) (string, error) + // Status returns structured information about the application + // installation or instance. + Status(*context.T, ...rpc.CallOpt) (Status, error) +} + +// ApplicationClientStub adds universal methods to ApplicationClientMethods. +type ApplicationClientStub interface { + ApplicationClientMethods + rpc.UniversalServiceMethods +} + +// ApplicationClient returns a client stub for Application. +func ApplicationClient(name string) ApplicationClientStub { + return implApplicationClientStub{name, permissions.ObjectClient(name)} +} + +type implApplicationClientStub struct { + name string + + permissions.ObjectClientStub +} + +func (c implApplicationClientStub) Install(ctx *context.T, i0 string, i1 Config, i2 application.Packages, opts ...rpc.CallOpt) (o0 string, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Install", []interface{}{i0, i1, i2}, []interface{}{&o0}, opts...) + return +} + +func (c implApplicationClientStub) Uninstall(ctx *context.T, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Uninstall", nil, nil, opts...) + return +} + +func (c implApplicationClientStub) Instantiate(ctx *context.T, opts ...rpc.CallOpt) (ocall ApplicationInstantiateClientCall, err error) { + var call rpc.ClientCall + if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "Instantiate", nil, opts...); err != nil { + return + } + ocall = &implApplicationInstantiateClientCall{ClientCall: call} + return +} + +func (c implApplicationClientStub) Delete(ctx *context.T, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Delete", nil, nil, opts...) + return +} + +func (c implApplicationClientStub) Run(ctx *context.T, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Run", nil, nil, opts...) + return +} + +func (c implApplicationClientStub) Kill(ctx *context.T, i0 time.Duration, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Kill", []interface{}{i0}, nil, opts...) + return +} + +func (c implApplicationClientStub) Update(ctx *context.T, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Update", nil, nil, opts...) + return +} + +func (c implApplicationClientStub) UpdateTo(ctx *context.T, i0 string, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "UpdateTo", []interface{}{i0}, nil, opts...) + return +} + +func (c implApplicationClientStub) Revert(ctx *context.T, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Revert", nil, nil, opts...) + return +} + +func (c implApplicationClientStub) Debug(ctx *context.T, opts ...rpc.CallOpt) (o0 string, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Debug", nil, []interface{}{&o0}, opts...) + return +} + +func (c implApplicationClientStub) Status(ctx *context.T, opts ...rpc.CallOpt) (o0 Status, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Status", nil, []interface{}{&o0}, opts...) + return +} + +// ApplicationInstantiateClientStream is the client stream for Application.Instantiate. +type ApplicationInstantiateClientStream interface { + // RecvStream returns the receiver side of the Application.Instantiate client stream. + RecvStream() interface { + // Advance stages an item so that it may be retrieved via Value. Returns + // true iff there is an item to retrieve. Advance must be called before + // Value is called. May block if an item is not available. + Advance() bool + // Value returns the item that was staged by Advance. May panic if Advance + // returned false or was not called. Never blocks. + Value() BlessServerMessage + // Err returns any error encountered by Advance. Never blocks. + Err() error + } + // SendStream returns the send side of the Application.Instantiate client stream. + SendStream() interface { + // Send places the item onto the output stream. Returns errors + // encountered while sending, or if Send is called after Close or + // the stream has been canceled. Blocks if there is no buffer + // space; will unblock when buffer space is available or after + // the stream has been canceled. + Send(item BlessClientMessage) error + // Close indicates to the server that no more items will be sent; + // server Recv calls will receive io.EOF after all sent items. + // This is an optional call - e.g. a client might call Close if it + // needs to continue receiving items from the server after it's + // done sending. Returns errors encountered while closing, or if + // Close is called after the stream has been canceled. Like Send, + // blocks if there is no buffer space available. + Close() error + } +} + +// ApplicationInstantiateClientCall represents the call returned from Application.Instantiate. +type ApplicationInstantiateClientCall interface { + ApplicationInstantiateClientStream + // Finish performs the equivalent of SendStream().Close, then blocks until + // the server is done, and returns the positional return values for the call. + // + // Finish returns immediately if the call has been canceled; depending on the + // timing the output could either be an error signaling cancelation, or the + // valid positional return values from the server. + // + // Calling Finish is mandatory for releasing stream resources, unless the call + // has been canceled or any of the other methods return an error. Finish should + // be called at most once. + Finish() (string, error) +} + +type implApplicationInstantiateClientCall struct { + rpc.ClientCall + valRecv BlessServerMessage + errRecv error +} + +func (c *implApplicationInstantiateClientCall) RecvStream() interface { + Advance() bool + Value() BlessServerMessage + Err() error +} { + return implApplicationInstantiateClientCallRecv{c} +} + +type implApplicationInstantiateClientCallRecv struct { + c *implApplicationInstantiateClientCall +} + +func (c implApplicationInstantiateClientCallRecv) Advance() bool { + c.c.errRecv = c.c.Recv(&c.c.valRecv) + return c.c.errRecv == nil +} +func (c implApplicationInstantiateClientCallRecv) Value() BlessServerMessage { + return c.c.valRecv +} +func (c implApplicationInstantiateClientCallRecv) Err() error { + if c.c.errRecv == io.EOF { + return nil + } + return c.c.errRecv +} +func (c *implApplicationInstantiateClientCall) SendStream() interface { + Send(item BlessClientMessage) error + Close() error +} { + return implApplicationInstantiateClientCallSend{c} +} + +type implApplicationInstantiateClientCallSend struct { + c *implApplicationInstantiateClientCall +} + +func (c implApplicationInstantiateClientCallSend) Send(item BlessClientMessage) error { + return c.c.Send(item) +} +func (c implApplicationInstantiateClientCallSend) Close() error { + return c.c.CloseSend() +} +func (c *implApplicationInstantiateClientCall) Finish() (o0 string, err error) { + err = c.ClientCall.Finish(&o0) + return +} + +// ApplicationServerMethods is the interface a server writer +// implements for Application. +// +// Application can be used to manage applications on a device. This interface +// will be invoked using an object name that identifies the application and its +// installations and instances where applicable. +// +// An application is defined by a title. An application can have multiple +// installations on a device. The installations are grouped under the same +// application, but are otherwise independent of each other. Each installation +// can have zero or more instances (which can be running or not). The instances +// are independent of each other, and do not share state (like local storage). +// Interaction among instances should occur via Vanadium RPC, facilitated by the +// local mounttable. +// +// The device manager supports versioning of applications. Each installation +// maintains a tree of versions, where a version is defined by a specific +// envelope. The tree structure comes from 'previous version' references: each +// version (except the initial installation version) maintains a reference to +// the version that preceded it. The installation maintains a current version +// reference that is used for new instances. Each update operation on the +// installation creates a new version, sets the previous reference of the new +// version to the current version, and then updates the current version to refer +// to the new version. Each revert operation on the installation sets the +// current version to the previous version of the current version. Each +// instance maintains a current version reference that is used to run the +// instance. The initial version of the instance is set to the current version +// of the installation at the time of instantiation. Each update operation on +// the instance updates the instance's current version to the current version of +// the installation. Each revert operation on the instance updates the +// instance's current version to the previous version of the instance's version. +// +// The Application interface methods can be divided based on their intended +// receiver: +// +// 1) Method receiver is an application: +// - Install() +// +// 2) Method receiver is an application installation: +// - Instantiate() +// - Uninstall() +// +// 3) Method receiver is an application instance: +// - Run() +// - Kill() +// - Delete() +// +// 4) Method receiver is an application installation or instance: +// - Update() +// - Revert() +// +// The following methods complement one another: +// - Install() and Uninstall() +// - Instantiate() and Delete() +// - Run() and Kill() +// - Update() and Revert() +// +// +// +// Examples: +// +// Install Google Maps on the device. +// device/apps.Install("/google.com/appstore/maps", nil, nil) --> "google maps/0" +// +// Create and start an instance of the previously installed maps application +// installation. +// device/apps/google maps/0.Instantiate() --> { "0" } +// device/apps/google maps/0/0.Run() +// +// Create and start a second instance of the previously installed maps +// application installation. +// device/apps/google maps/0.Instantiate() --> { "1" } +// device/apps/google maps/0/1.Run() +// +// Kill and delete the first instance previously started. +// device/apps/google maps/0/0.Kill() +// device/apps/google maps/0/0.Delete() +// +// Install a second Google Maps installation. +// device/apps.Install("/google.com/appstore/maps", nil, nil) --> "google maps/1" +// +// Update the second maps installation to the latest version available. +// device/apps/google maps/1.Update() +// +// Update the first maps installation to a specific version. +// device/apps/google maps/0.UpdateTo("/google.com/appstore/beta/maps") +// +// Finally, an application installation instance can be in one of three abstract +// states: 1) "does not exist/deleted", 2) "running", or 3) "not-running". The +// interface methods transition between these abstract states using the +// following state machine: +// +// apply(Instantiate(), "does not exist") = "not-running" +// apply(Run(), "not-running") = "running" +// apply(Kill(), "running") = "not-running" +// apply(Delete(), "not-running") = "deleted" +type ApplicationServerMethods interface { + // Object provides access control for Vanadium objects. + // + // Vanadium services implementing dynamic access control would typically embed + // this interface and tag additional methods defined by the service with one of + // Admin, Read, Write, Resolve etc. For example, the VDL definition of the + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // import "v.io/v23/services/permissions" + // + // type MyObject interface { + // permissions.Object + // MyRead() (string, error) {access.Read} + // MyWrite(string) error {access.Write} + // } + // + // If the set of pre-defined tags is insufficient, services may define their + // own tag type and annotate all methods with this new type. + // + // Instead of embedding this Object interface, define SetPermissions and + // GetPermissions in their own interface. Authorization policies will typically + // respect annotations of a single type. For example, the VDL definition of an + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // + // type MyTag string + // + // const ( + // Blue = MyTag("Blue") + // Red = MyTag("Red") + // ) + // + // type MyObject interface { + // MyMethod() (string, error) {Blue} + // + // // Allow clients to change access via the access.Object interface: + // SetPermissions(perms access.Permissions, version string) error {Red} + // GetPermissions() (perms access.Permissions, version string, err error) {Blue} + // } + permissions.ObjectServerMethods + // Install installs the application identified by the first argument and + // returns an object name suffix that identifies the new installation. + // + // The name argument should be an object name for an application + // envelope. The service it identifies must implement + // repository.Application, and is expected to return either the + // requested version (if the object name encodes a specific version), or + // otherwise the latest available version, as appropriate. This object + // name will be used by default by the Update method, as a source for + // updated application envelopes (can be overriden by setting + // AppOriginConfigKey in the config). + // + // The config argument specifies config settings that will take + // precedence over those present in the application envelope. + // + // The packages argument specifies packages to be installed in addition + // to those specified in the envelope. If a package in the envelope has + // the same key, the package in the packages argument takes precedence. + // + // The returned suffix, when appended to the name used to reach the + // receiver for Install, can be used to control the installation object. + // The suffix will contain the title of the application as a prefix, + // which can then be used to control all the installations of the given + // application. + // TODO(rjkroege): Use customized labels. + Install(_ *context.T, _ rpc.ServerCall, name string, config Config, packages application.Packages) (string, error) + // Uninstall uninstalls an application installation. + // The installation must be in state Active. + Uninstall(*context.T, rpc.ServerCall) error + // Instantiate creates an instance of an application installation. + // The installation must be in state Active. + // + // The server sends the application instance's Public Key on the stream. + // When the client receives the Public Key it must send Blessings back + // to the server. When the instance is created, the server returns the + // instance name to the client. + // + // Client Server + // "object".Instantiate() --> + // <-- InstancePublicKey + // AppBlessings --> + // <-- return InstanceName + Instantiate(*context.T, ApplicationInstantiateServerCall) (string, error) + // Delete deletes an instance. Once deleted, the instance cannot be + // revived. + // The instance must be in state NotRunning. + // + // If called against a Device, causes the Device to shut itself down. + Delete(*context.T, rpc.ServerCall) error + // Run begins execution of an application instance. + // The instance must be in state NotRunning. + Run(*context.T, rpc.ServerCall) error + // Kill attempts a clean shutdown an of application instance. + // The instance must be in state Running. + // + // If the deadline is non-zero and the instance in question is still + // running after the given deadline, shutdown of the instance is + // enforced. + // + // If called against a Device, causes the Device to stop itself (which + // may or may not result in a restart depending on the device manager + // setup). + Kill(_ *context.T, _ rpc.ServerCall, deadline time.Duration) error + // Update updates an application installation's version to a new version + // created from the envelope at the object name provided during Install. + // If the new application envelope contains a different application + // title, the update does not occur, and an error is returned. The + // installation must be in state Active. + // + // Update updates an application instance's version to the current + // installation version. The instance must be in state NotRunning. + Update(*context.T, rpc.ServerCall) error + // UpdateTo updates the application installation(s) to the application + // specified by the object name argument. If the new application + // envelope contains a different application title, the update does not + // occur, and an error is returned. + // The installation must be in state Active. + UpdateTo(_ *context.T, _ rpc.ServerCall, name string) error + // Revert reverts an application installation's version to the previous + // version of its current version. The installation must be in state + // Active. + // + // Revert reverts an application instance's version to the previous + // version of its current version. The instance must be in state + // NotRunning. + Revert(*context.T, rpc.ServerCall) error + // Debug returns debug information about the application installation or + // instance. This is generally highly implementation-specific, and + // presented in an unstructured form. No guarantees are given about the + // stability of the format, and parsing it programmatically is + // specifically discouraged. + Debug(*context.T, rpc.ServerCall) (string, error) + // Status returns structured information about the application + // installation or instance. + Status(*context.T, rpc.ServerCall) (Status, error) +} + +// ApplicationServerStubMethods is the server interface containing +// Application methods, as expected by rpc.Server. +// The only difference between this interface and ApplicationServerMethods +// is the streaming methods. +type ApplicationServerStubMethods interface { + // Object provides access control for Vanadium objects. + // + // Vanadium services implementing dynamic access control would typically embed + // this interface and tag additional methods defined by the service with one of + // Admin, Read, Write, Resolve etc. For example, the VDL definition of the + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // import "v.io/v23/services/permissions" + // + // type MyObject interface { + // permissions.Object + // MyRead() (string, error) {access.Read} + // MyWrite(string) error {access.Write} + // } + // + // If the set of pre-defined tags is insufficient, services may define their + // own tag type and annotate all methods with this new type. + // + // Instead of embedding this Object interface, define SetPermissions and + // GetPermissions in their own interface. Authorization policies will typically + // respect annotations of a single type. For example, the VDL definition of an + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // + // type MyTag string + // + // const ( + // Blue = MyTag("Blue") + // Red = MyTag("Red") + // ) + // + // type MyObject interface { + // MyMethod() (string, error) {Blue} + // + // // Allow clients to change access via the access.Object interface: + // SetPermissions(perms access.Permissions, version string) error {Red} + // GetPermissions() (perms access.Permissions, version string, err error) {Blue} + // } + permissions.ObjectServerStubMethods + // Install installs the application identified by the first argument and + // returns an object name suffix that identifies the new installation. + // + // The name argument should be an object name for an application + // envelope. The service it identifies must implement + // repository.Application, and is expected to return either the + // requested version (if the object name encodes a specific version), or + // otherwise the latest available version, as appropriate. This object + // name will be used by default by the Update method, as a source for + // updated application envelopes (can be overriden by setting + // AppOriginConfigKey in the config). + // + // The config argument specifies config settings that will take + // precedence over those present in the application envelope. + // + // The packages argument specifies packages to be installed in addition + // to those specified in the envelope. If a package in the envelope has + // the same key, the package in the packages argument takes precedence. + // + // The returned suffix, when appended to the name used to reach the + // receiver for Install, can be used to control the installation object. + // The suffix will contain the title of the application as a prefix, + // which can then be used to control all the installations of the given + // application. + // TODO(rjkroege): Use customized labels. + Install(_ *context.T, _ rpc.ServerCall, name string, config Config, packages application.Packages) (string, error) + // Uninstall uninstalls an application installation. + // The installation must be in state Active. + Uninstall(*context.T, rpc.ServerCall) error + // Instantiate creates an instance of an application installation. + // The installation must be in state Active. + // + // The server sends the application instance's Public Key on the stream. + // When the client receives the Public Key it must send Blessings back + // to the server. When the instance is created, the server returns the + // instance name to the client. + // + // Client Server + // "object".Instantiate() --> + // <-- InstancePublicKey + // AppBlessings --> + // <-- return InstanceName + Instantiate(*context.T, *ApplicationInstantiateServerCallStub) (string, error) + // Delete deletes an instance. Once deleted, the instance cannot be + // revived. + // The instance must be in state NotRunning. + // + // If called against a Device, causes the Device to shut itself down. + Delete(*context.T, rpc.ServerCall) error + // Run begins execution of an application instance. + // The instance must be in state NotRunning. + Run(*context.T, rpc.ServerCall) error + // Kill attempts a clean shutdown an of application instance. + // The instance must be in state Running. + // + // If the deadline is non-zero and the instance in question is still + // running after the given deadline, shutdown of the instance is + // enforced. + // + // If called against a Device, causes the Device to stop itself (which + // may or may not result in a restart depending on the device manager + // setup). + Kill(_ *context.T, _ rpc.ServerCall, deadline time.Duration) error + // Update updates an application installation's version to a new version + // created from the envelope at the object name provided during Install. + // If the new application envelope contains a different application + // title, the update does not occur, and an error is returned. The + // installation must be in state Active. + // + // Update updates an application instance's version to the current + // installation version. The instance must be in state NotRunning. + Update(*context.T, rpc.ServerCall) error + // UpdateTo updates the application installation(s) to the application + // specified by the object name argument. If the new application + // envelope contains a different application title, the update does not + // occur, and an error is returned. + // The installation must be in state Active. + UpdateTo(_ *context.T, _ rpc.ServerCall, name string) error + // Revert reverts an application installation's version to the previous + // version of its current version. The installation must be in state + // Active. + // + // Revert reverts an application instance's version to the previous + // version of its current version. The instance must be in state + // NotRunning. + Revert(*context.T, rpc.ServerCall) error + // Debug returns debug information about the application installation or + // instance. This is generally highly implementation-specific, and + // presented in an unstructured form. No guarantees are given about the + // stability of the format, and parsing it programmatically is + // specifically discouraged. + Debug(*context.T, rpc.ServerCall) (string, error) + // Status returns structured information about the application + // installation or instance. + Status(*context.T, rpc.ServerCall) (Status, error) +} + +// ApplicationServerStub adds universal methods to ApplicationServerStubMethods. +type ApplicationServerStub interface { + ApplicationServerStubMethods + // Describe the Application interfaces. + Describe__() []rpc.InterfaceDesc +} + +// ApplicationServer returns a server stub for Application. +// It converts an implementation of ApplicationServerMethods into +// an object that may be used by rpc.Server. +func ApplicationServer(impl ApplicationServerMethods) ApplicationServerStub { + stub := implApplicationServerStub{ + impl: impl, + ObjectServerStub: permissions.ObjectServer(impl), + } + // Initialize GlobState; always check the stub itself first, to handle the + // case where the user has the Glob method defined in their VDL source. + if gs := rpc.NewGlobState(stub); gs != nil { + stub.gs = gs + } else if gs := rpc.NewGlobState(impl); gs != nil { + stub.gs = gs + } + return stub +} + +type implApplicationServerStub struct { + impl ApplicationServerMethods + permissions.ObjectServerStub + gs *rpc.GlobState +} + +func (s implApplicationServerStub) Install(ctx *context.T, call rpc.ServerCall, i0 string, i1 Config, i2 application.Packages) (string, error) { + return s.impl.Install(ctx, call, i0, i1, i2) +} + +func (s implApplicationServerStub) Uninstall(ctx *context.T, call rpc.ServerCall) error { + return s.impl.Uninstall(ctx, call) +} + +func (s implApplicationServerStub) Instantiate(ctx *context.T, call *ApplicationInstantiateServerCallStub) (string, error) { + return s.impl.Instantiate(ctx, call) +} + +func (s implApplicationServerStub) Delete(ctx *context.T, call rpc.ServerCall) error { + return s.impl.Delete(ctx, call) +} + +func (s implApplicationServerStub) Run(ctx *context.T, call rpc.ServerCall) error { + return s.impl.Run(ctx, call) +} + +func (s implApplicationServerStub) Kill(ctx *context.T, call rpc.ServerCall, i0 time.Duration) error { + return s.impl.Kill(ctx, call, i0) +} + +func (s implApplicationServerStub) Update(ctx *context.T, call rpc.ServerCall) error { + return s.impl.Update(ctx, call) +} + +func (s implApplicationServerStub) UpdateTo(ctx *context.T, call rpc.ServerCall, i0 string) error { + return s.impl.UpdateTo(ctx, call, i0) +} + +func (s implApplicationServerStub) Revert(ctx *context.T, call rpc.ServerCall) error { + return s.impl.Revert(ctx, call) +} + +func (s implApplicationServerStub) Debug(ctx *context.T, call rpc.ServerCall) (string, error) { + return s.impl.Debug(ctx, call) +} + +func (s implApplicationServerStub) Status(ctx *context.T, call rpc.ServerCall) (Status, error) { + return s.impl.Status(ctx, call) +} + +func (s implApplicationServerStub) Globber() *rpc.GlobState { + return s.gs +} + +func (s implApplicationServerStub) Describe__() []rpc.InterfaceDesc { + return []rpc.InterfaceDesc{ApplicationDesc, permissions.ObjectDesc} +} + +// ApplicationDesc describes the Application interface. +var ApplicationDesc rpc.InterfaceDesc = descApplication + +// descApplication hides the desc to keep godoc clean. +var descApplication = rpc.InterfaceDesc{ + Name: "Application", + PkgPath: "v.io/v23/services/device", + Doc: "// Application can be used to manage applications on a device. This interface\n// will be invoked using an object name that identifies the application and its\n// installations and instances where applicable.\n//\n// An application is defined by a title. An application can have multiple\n// installations on a device. The installations are grouped under the same\n// application, but are otherwise independent of each other. Each installation\n// can have zero or more instances (which can be running or not). The instances\n// are independent of each other, and do not share state (like local storage).\n// Interaction among instances should occur via Vanadium RPC, facilitated by the\n// local mounttable.\n//\n// The device manager supports versioning of applications. Each installation\n// maintains a tree of versions, where a version is defined by a specific\n// envelope. The tree structure comes from 'previous version' references: each\n// version (except the initial installation version) maintains a reference to\n// the version that preceded it. The installation maintains a current version\n// reference that is used for new instances. Each update operation on the\n// installation creates a new version, sets the previous reference of the new\n// version to the current version, and then updates the current version to refer\n// to the new version. Each revert operation on the installation sets the\n// current version to the previous version of the current version. Each\n// instance maintains a current version reference that is used to run the\n// instance. The initial version of the instance is set to the current version\n// of the installation at the time of instantiation. Each update operation on\n// the instance updates the instance's current version to the current version of\n// the installation. Each revert operation on the instance updates the\n// instance's current version to the previous version of the instance's version.\n//\n// The Application interface methods can be divided based on their intended\n// receiver:\n//\n// 1) Method receiver is an application:\n// - Install()\n//\n// 2) Method receiver is an application installation:\n// - Instantiate()\n// - Uninstall()\n//\n// 3) Method receiver is an application instance:\n// - Run()\n// - Kill()\n// - Delete()\n//\n// 4) Method receiver is an application installation or instance:\n// - Update()\n// - Revert()\n//\n// The following methods complement one another:\n// - Install() and Uninstall()\n// - Instantiate() and Delete()\n// - Run() and Kill()\n// - Update() and Revert()\n//\n//\n//\n// Examples:\n//\n// Install Google Maps on the device.\n// device/apps.Install(\"/google.com/appstore/maps\", nil, nil) --> \"google maps/0\"\n//\n// Create and start an instance of the previously installed maps application\n// installation.\n// device/apps/google maps/0.Instantiate() --> { \"0\" }\n// device/apps/google maps/0/0.Run()\n//\n// Create and start a second instance of the previously installed maps\n// application installation.\n// device/apps/google maps/0.Instantiate() --> { \"1\" }\n// device/apps/google maps/0/1.Run()\n//\n// Kill and delete the first instance previously started.\n// device/apps/google maps/0/0.Kill()\n// device/apps/google maps/0/0.Delete()\n//\n// Install a second Google Maps installation.\n// device/apps.Install(\"/google.com/appstore/maps\", nil, nil) --> \"google maps/1\"\n//\n// Update the second maps installation to the latest version available.\n// device/apps/google maps/1.Update()\n//\n// Update the first maps installation to a specific version.\n// device/apps/google maps/0.UpdateTo(\"/google.com/appstore/beta/maps\")\n//\n// Finally, an application installation instance can be in one of three abstract\n// states: 1) \"does not exist/deleted\", 2) \"running\", or 3) \"not-running\". The\n// interface methods transition between these abstract states using the\n// following state machine:\n//\n// apply(Instantiate(), \"does not exist\") = \"not-running\"\n// apply(Run(), \"not-running\") = \"running\"\n// apply(Kill(), \"running\") = \"not-running\"\n// apply(Delete(), \"not-running\") = \"deleted\"", + Embeds: []rpc.EmbedDesc{ + {"Object", "v.io/v23/services/permissions", "// Object provides access control for Vanadium objects.\n//\n// Vanadium services implementing dynamic access control would typically embed\n// this interface and tag additional methods defined by the service with one of\n// Admin, Read, Write, Resolve etc. For example, the VDL definition of the\n// object would be:\n//\n// package mypackage\n//\n// import \"v.io/v23/security/access\"\n// import \"v.io/v23/services/permissions\"\n//\n// type MyObject interface {\n// permissions.Object\n// MyRead() (string, error) {access.Read}\n// MyWrite(string) error {access.Write}\n// }\n//\n// If the set of pre-defined tags is insufficient, services may define their\n// own tag type and annotate all methods with this new type.\n//\n// Instead of embedding this Object interface, define SetPermissions and\n// GetPermissions in their own interface. Authorization policies will typically\n// respect annotations of a single type. For example, the VDL definition of an\n// object would be:\n//\n// package mypackage\n//\n// import \"v.io/v23/security/access\"\n//\n// type MyTag string\n//\n// const (\n// Blue = MyTag(\"Blue\")\n// Red = MyTag(\"Red\")\n// )\n//\n// type MyObject interface {\n// MyMethod() (string, error) {Blue}\n//\n// // Allow clients to change access via the access.Object interface:\n// SetPermissions(perms access.Permissions, version string) error {Red}\n// GetPermissions() (perms access.Permissions, version string, err error) {Blue}\n// }"}, + }, + Methods: []rpc.MethodDesc{ + { + Name: "Install", + Doc: "// Install installs the application identified by the first argument and\n// returns an object name suffix that identifies the new installation.\n//\n// The name argument should be an object name for an application\n// envelope. The service it identifies must implement\n// repository.Application, and is expected to return either the\n// requested version (if the object name encodes a specific version), or\n// otherwise the latest available version, as appropriate. This object\n// name will be used by default by the Update method, as a source for\n// updated application envelopes (can be overriden by setting\n// AppOriginConfigKey in the config).\n//\n// The config argument specifies config settings that will take\n// precedence over those present in the application envelope.\n//\n// The packages argument specifies packages to be installed in addition\n// to those specified in the envelope. If a package in the envelope has\n// the same key, the package in the packages argument takes precedence.\n//\n// The returned suffix, when appended to the name used to reach the\n// receiver for Install, can be used to control the installation object.\n// The suffix will contain the title of the application as a prefix,\n// which can then be used to control all the installations of the given\n// application.\n// TODO(rjkroege): Use customized labels.", + InArgs: []rpc.ArgDesc{ + {"name", ``}, // string + {"config", ``}, // Config + {"packages", ``}, // application.Packages + }, + OutArgs: []rpc.ArgDesc{ + {"", ``}, // string + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))}, + }, + { + Name: "Uninstall", + Doc: "// Uninstall uninstalls an application installation.\n// The installation must be in state Active.", + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))}, + }, + { + Name: "Instantiate", + Doc: "// Instantiate creates an instance of an application installation.\n// The installation must be in state Active.\n//\n// The server sends the application instance's Public Key on the stream.\n// When the client receives the Public Key it must send Blessings back\n// to the server. When the instance is created, the server returns the\n// instance name to the client.\n//\n// Client Server\n// \"object\".Instantiate() -->\n// <-- InstancePublicKey\n// AppBlessings -->\n// <-- return InstanceName", + OutArgs: []rpc.ArgDesc{ + {"", ``}, // string + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))}, + }, + { + Name: "Delete", + Doc: "// Delete deletes an instance. Once deleted, the instance cannot be\n// revived.\n// The instance must be in state NotRunning.\n//\n// If called against a Device, causes the Device to shut itself down.", + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))}, + }, + { + Name: "Run", + Doc: "// Run begins execution of an application instance.\n// The instance must be in state NotRunning.", + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))}, + }, + { + Name: "Kill", + Doc: "// Kill attempts a clean shutdown an of application instance.\n// The instance must be in state Running.\n//\n// If the deadline is non-zero and the instance in question is still\n// running after the given deadline, shutdown of the instance is\n// enforced.\n//\n// If called against a Device, causes the Device to stop itself (which\n// may or may not result in a restart depending on the device manager\n// setup).", + InArgs: []rpc.ArgDesc{ + {"deadline", ``}, // time.Duration + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))}, + }, + { + Name: "Update", + Doc: "// Update updates an application installation's version to a new version\n// created from the envelope at the object name provided during Install.\n// If the new application envelope contains a different application\n// title, the update does not occur, and an error is returned. The\n// installation must be in state Active.\n//\n// Update updates an application instance's version to the current\n// installation version. The instance must be in state NotRunning.", + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))}, + }, + { + Name: "UpdateTo", + Doc: "// UpdateTo updates the application installation(s) to the application\n// specified by the object name argument. If the new application\n// envelope contains a different application title, the update does not\n// occur, and an error is returned.\n// The installation must be in state Active.", + InArgs: []rpc.ArgDesc{ + {"name", ``}, // string + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))}, + }, + { + Name: "Revert", + Doc: "// Revert reverts an application installation's version to the previous\n// version of its current version. The installation must be in state\n// Active.\n//\n// Revert reverts an application instance's version to the previous\n// version of its current version. The instance must be in state\n// NotRunning.", + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))}, + }, + { + Name: "Debug", + Doc: "// Debug returns debug information about the application installation or\n// instance. This is generally highly implementation-specific, and\n// presented in an unstructured form. No guarantees are given about the\n// stability of the format, and parsing it programmatically is\n// specifically discouraged.", + OutArgs: []rpc.ArgDesc{ + {"", ``}, // string + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Debug"))}, + }, + { + Name: "Status", + Doc: "// Status returns structured information about the application\n// installation or instance.", + OutArgs: []rpc.ArgDesc{ + {"", ``}, // Status + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))}, + }, + }, +} + +// ApplicationInstantiateServerStream is the server stream for Application.Instantiate. +type ApplicationInstantiateServerStream interface { + // RecvStream returns the receiver side of the Application.Instantiate server stream. + RecvStream() interface { + // Advance stages an item so that it may be retrieved via Value. Returns + // true iff there is an item to retrieve. Advance must be called before + // Value is called. May block if an item is not available. + Advance() bool + // Value returns the item that was staged by Advance. May panic if Advance + // returned false or was not called. Never blocks. + Value() BlessClientMessage + // Err returns any error encountered by Advance. Never blocks. + Err() error + } + // SendStream returns the send side of the Application.Instantiate server stream. + SendStream() interface { + // Send places the item onto the output stream. Returns errors encountered + // while sending. Blocks if there is no buffer space; will unblock when + // buffer space is available. + Send(item BlessServerMessage) error + } +} + +// ApplicationInstantiateServerCall represents the context passed to Application.Instantiate. +type ApplicationInstantiateServerCall interface { + rpc.ServerCall + ApplicationInstantiateServerStream +} + +// ApplicationInstantiateServerCallStub is a wrapper that converts rpc.StreamServerCall into +// a typesafe stub that implements ApplicationInstantiateServerCall. +type ApplicationInstantiateServerCallStub struct { + rpc.StreamServerCall + valRecv BlessClientMessage + errRecv error +} + +// Init initializes ApplicationInstantiateServerCallStub from rpc.StreamServerCall. +func (s *ApplicationInstantiateServerCallStub) Init(call rpc.StreamServerCall) { + s.StreamServerCall = call +} + +// RecvStream returns the receiver side of the Application.Instantiate server stream. +func (s *ApplicationInstantiateServerCallStub) RecvStream() interface { + Advance() bool + Value() BlessClientMessage + Err() error +} { + return implApplicationInstantiateServerCallRecv{s} +} + +type implApplicationInstantiateServerCallRecv struct { + s *ApplicationInstantiateServerCallStub +} + +func (s implApplicationInstantiateServerCallRecv) Advance() bool { + s.s.errRecv = s.s.Recv(&s.s.valRecv) + return s.s.errRecv == nil +} +func (s implApplicationInstantiateServerCallRecv) Value() BlessClientMessage { + return s.s.valRecv +} +func (s implApplicationInstantiateServerCallRecv) Err() error { + if s.s.errRecv == io.EOF { + return nil + } + return s.s.errRecv +} + +// SendStream returns the send side of the Application.Instantiate server stream. +func (s *ApplicationInstantiateServerCallStub) SendStream() interface { + Send(item BlessServerMessage) error +} { + return implApplicationInstantiateServerCallSend{s} +} + +type implApplicationInstantiateServerCallSend struct { + s *ApplicationInstantiateServerCallStub +} + +func (s implApplicationInstantiateServerCallSend) Send(item BlessServerMessage) error { + return s.s.Send(item) +} + +// ClaimableClientMethods is the client interface +// containing Claimable methods. +// +// Claimable represents an uninitialized device with no owner +// (i.e., a device that has no blessings). +// +// Claim is used to claim ownership by blessing the device's private key. +// Devices that have provided a pairing token to the claimer through an +// out-of-band communication channel (eg: display/email) would expect this +// pairing token to be replayed by the claimer. +// +// Once claimed, the device will export the "Device" interface and all methods +// will be restricted to the claimer. +// +// The blessings that the device is to be claimed with is provided +// via the ipc.Granter option in Go. +type ClaimableClientMethods interface { + Claim(_ *context.T, pairingToken string, _ ...rpc.CallOpt) error +} + +// ClaimableClientStub adds universal methods to ClaimableClientMethods. +type ClaimableClientStub interface { + ClaimableClientMethods + rpc.UniversalServiceMethods +} + +// ClaimableClient returns a client stub for Claimable. +func ClaimableClient(name string) ClaimableClientStub { + return implClaimableClientStub{name} +} + +type implClaimableClientStub struct { + name string +} + +func (c implClaimableClientStub) Claim(ctx *context.T, i0 string, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Claim", []interface{}{i0}, nil, opts...) + return +} + +// ClaimableServerMethods is the interface a server writer +// implements for Claimable. +// +// Claimable represents an uninitialized device with no owner +// (i.e., a device that has no blessings). +// +// Claim is used to claim ownership by blessing the device's private key. +// Devices that have provided a pairing token to the claimer through an +// out-of-band communication channel (eg: display/email) would expect this +// pairing token to be replayed by the claimer. +// +// Once claimed, the device will export the "Device" interface and all methods +// will be restricted to the claimer. +// +// The blessings that the device is to be claimed with is provided +// via the ipc.Granter option in Go. +type ClaimableServerMethods interface { + Claim(_ *context.T, _ rpc.ServerCall, pairingToken string) error +} + +// ClaimableServerStubMethods is the server interface containing +// Claimable methods, as expected by rpc.Server. +// There is no difference between this interface and ClaimableServerMethods +// since there are no streaming methods. +type ClaimableServerStubMethods ClaimableServerMethods + +// ClaimableServerStub adds universal methods to ClaimableServerStubMethods. +type ClaimableServerStub interface { + ClaimableServerStubMethods + // Describe the Claimable interfaces. + Describe__() []rpc.InterfaceDesc +} + +// ClaimableServer returns a server stub for Claimable. +// It converts an implementation of ClaimableServerMethods into +// an object that may be used by rpc.Server. +func ClaimableServer(impl ClaimableServerMethods) ClaimableServerStub { + stub := implClaimableServerStub{ + impl: impl, + } + // Initialize GlobState; always check the stub itself first, to handle the + // case where the user has the Glob method defined in their VDL source. + if gs := rpc.NewGlobState(stub); gs != nil { + stub.gs = gs + } else if gs := rpc.NewGlobState(impl); gs != nil { + stub.gs = gs + } + return stub +} + +type implClaimableServerStub struct { + impl ClaimableServerMethods + gs *rpc.GlobState +} + +func (s implClaimableServerStub) Claim(ctx *context.T, call rpc.ServerCall, i0 string) error { + return s.impl.Claim(ctx, call, i0) +} + +func (s implClaimableServerStub) Globber() *rpc.GlobState { + return s.gs +} + +func (s implClaimableServerStub) Describe__() []rpc.InterfaceDesc { + return []rpc.InterfaceDesc{ClaimableDesc} +} + +// ClaimableDesc describes the Claimable interface. +var ClaimableDesc rpc.InterfaceDesc = descClaimable + +// descClaimable hides the desc to keep godoc clean. +var descClaimable = rpc.InterfaceDesc{ + Name: "Claimable", + PkgPath: "v.io/v23/services/device", + Doc: "// Claimable represents an uninitialized device with no owner\n// (i.e., a device that has no blessings).\n//\n// Claim is used to claim ownership by blessing the device's private key.\n// Devices that have provided a pairing token to the claimer through an\n// out-of-band communication channel (eg: display/email) would expect this\n// pairing token to be replayed by the claimer.\n//\n// Once claimed, the device will export the \"Device\" interface and all methods\n// will be restricted to the claimer.\n//\n// The blessings that the device is to be claimed with is provided\n// via the ipc.Granter option in Go.", + Methods: []rpc.MethodDesc{ + { + Name: "Claim", + InArgs: []rpc.ArgDesc{ + {"pairingToken", ``}, // string + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))}, + }, + }, +} + +// DeviceClientMethods is the client interface +// containing Device methods. +// +// Device can be used to manage a device remotely using an object name that +// identifies it. +type DeviceClientMethods interface { + // Application can be used to manage applications on a device. This interface + // will be invoked using an object name that identifies the application and its + // installations and instances where applicable. + // + // An application is defined by a title. An application can have multiple + // installations on a device. The installations are grouped under the same + // application, but are otherwise independent of each other. Each installation + // can have zero or more instances (which can be running or not). The instances + // are independent of each other, and do not share state (like local storage). + // Interaction among instances should occur via Vanadium RPC, facilitated by the + // local mounttable. + // + // The device manager supports versioning of applications. Each installation + // maintains a tree of versions, where a version is defined by a specific + // envelope. The tree structure comes from 'previous version' references: each + // version (except the initial installation version) maintains a reference to + // the version that preceded it. The installation maintains a current version + // reference that is used for new instances. Each update operation on the + // installation creates a new version, sets the previous reference of the new + // version to the current version, and then updates the current version to refer + // to the new version. Each revert operation on the installation sets the + // current version to the previous version of the current version. Each + // instance maintains a current version reference that is used to run the + // instance. The initial version of the instance is set to the current version + // of the installation at the time of instantiation. Each update operation on + // the instance updates the instance's current version to the current version of + // the installation. Each revert operation on the instance updates the + // instance's current version to the previous version of the instance's version. + // + // The Application interface methods can be divided based on their intended + // receiver: + // + // 1) Method receiver is an application: + // - Install() + // + // 2) Method receiver is an application installation: + // - Instantiate() + // - Uninstall() + // + // 3) Method receiver is an application instance: + // - Run() + // - Kill() + // - Delete() + // + // 4) Method receiver is an application installation or instance: + // - Update() + // - Revert() + // + // The following methods complement one another: + // - Install() and Uninstall() + // - Instantiate() and Delete() + // - Run() and Kill() + // - Update() and Revert() + // + // + // + // Examples: + // + // Install Google Maps on the device. + // device/apps.Install("/google.com/appstore/maps", nil, nil) --> "google maps/0" + // + // Create and start an instance of the previously installed maps application + // installation. + // device/apps/google maps/0.Instantiate() --> { "0" } + // device/apps/google maps/0/0.Run() + // + // Create and start a second instance of the previously installed maps + // application installation. + // device/apps/google maps/0.Instantiate() --> { "1" } + // device/apps/google maps/0/1.Run() + // + // Kill and delete the first instance previously started. + // device/apps/google maps/0/0.Kill() + // device/apps/google maps/0/0.Delete() + // + // Install a second Google Maps installation. + // device/apps.Install("/google.com/appstore/maps", nil, nil) --> "google maps/1" + // + // Update the second maps installation to the latest version available. + // device/apps/google maps/1.Update() + // + // Update the first maps installation to a specific version. + // device/apps/google maps/0.UpdateTo("/google.com/appstore/beta/maps") + // + // Finally, an application installation instance can be in one of three abstract + // states: 1) "does not exist/deleted", 2) "running", or 3) "not-running". The + // interface methods transition between these abstract states using the + // following state machine: + // + // apply(Instantiate(), "does not exist") = "not-running" + // apply(Run(), "not-running") = "running" + // apply(Kill(), "running") = "not-running" + // apply(Delete(), "not-running") = "deleted" + ApplicationClientMethods + // Tidyable specifies that a service can be tidied. + tidyable.TidyableClientMethods + // Describe generates a description of the device. + Describe(*context.T, ...rpc.CallOpt) (Description, error) + // IsRunnable checks if the device can execute the given binary. + IsRunnable(_ *context.T, description binary.Description, _ ...rpc.CallOpt) (bool, error) + // Reset resets the device. If the deadline is non-zero and the device + // in question is still running after the given deadline expired, + // reset of the device is enforced. + Reset(_ *context.T, deadline time.Duration, _ ...rpc.CallOpt) error + // AssociateAccount associates a local system account name with the provided + // Vanadium identities. It replaces the existing association if one already exists for that + // identity. Setting an AccountName to "" removes the association for each + // listed identity. + AssociateAccount(_ *context.T, identityNames []string, accountName string, _ ...rpc.CallOpt) error + // ListAssociations returns all of the associations between Vanadium identities + // and system names. + ListAssociations(*context.T, ...rpc.CallOpt) ([]Association, error) +} + +// DeviceClientStub adds universal methods to DeviceClientMethods. +type DeviceClientStub interface { + DeviceClientMethods + rpc.UniversalServiceMethods +} + +// DeviceClient returns a client stub for Device. +func DeviceClient(name string) DeviceClientStub { + return implDeviceClientStub{name, ApplicationClient(name), tidyable.TidyableClient(name)} +} + +type implDeviceClientStub struct { + name string + + ApplicationClientStub + tidyable.TidyableClientStub +} + +func (c implDeviceClientStub) Describe(ctx *context.T, opts ...rpc.CallOpt) (o0 Description, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Describe", nil, []interface{}{&o0}, opts...) + return +} + +func (c implDeviceClientStub) IsRunnable(ctx *context.T, i0 binary.Description, opts ...rpc.CallOpt) (o0 bool, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "IsRunnable", []interface{}{i0}, []interface{}{&o0}, opts...) + return +} + +func (c implDeviceClientStub) Reset(ctx *context.T, i0 time.Duration, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Reset", []interface{}{i0}, nil, opts...) + return +} + +func (c implDeviceClientStub) AssociateAccount(ctx *context.T, i0 []string, i1 string, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "AssociateAccount", []interface{}{i0, i1}, nil, opts...) + return +} + +func (c implDeviceClientStub) ListAssociations(ctx *context.T, opts ...rpc.CallOpt) (o0 []Association, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "ListAssociations", nil, []interface{}{&o0}, opts...) + return +} + +// DeviceServerMethods is the interface a server writer +// implements for Device. +// +// Device can be used to manage a device remotely using an object name that +// identifies it. +type DeviceServerMethods interface { + // Application can be used to manage applications on a device. This interface + // will be invoked using an object name that identifies the application and its + // installations and instances where applicable. + // + // An application is defined by a title. An application can have multiple + // installations on a device. The installations are grouped under the same + // application, but are otherwise independent of each other. Each installation + // can have zero or more instances (which can be running or not). The instances + // are independent of each other, and do not share state (like local storage). + // Interaction among instances should occur via Vanadium RPC, facilitated by the + // local mounttable. + // + // The device manager supports versioning of applications. Each installation + // maintains a tree of versions, where a version is defined by a specific + // envelope. The tree structure comes from 'previous version' references: each + // version (except the initial installation version) maintains a reference to + // the version that preceded it. The installation maintains a current version + // reference that is used for new instances. Each update operation on the + // installation creates a new version, sets the previous reference of the new + // version to the current version, and then updates the current version to refer + // to the new version. Each revert operation on the installation sets the + // current version to the previous version of the current version. Each + // instance maintains a current version reference that is used to run the + // instance. The initial version of the instance is set to the current version + // of the installation at the time of instantiation. Each update operation on + // the instance updates the instance's current version to the current version of + // the installation. Each revert operation on the instance updates the + // instance's current version to the previous version of the instance's version. + // + // The Application interface methods can be divided based on their intended + // receiver: + // + // 1) Method receiver is an application: + // - Install() + // + // 2) Method receiver is an application installation: + // - Instantiate() + // - Uninstall() + // + // 3) Method receiver is an application instance: + // - Run() + // - Kill() + // - Delete() + // + // 4) Method receiver is an application installation or instance: + // - Update() + // - Revert() + // + // The following methods complement one another: + // - Install() and Uninstall() + // - Instantiate() and Delete() + // - Run() and Kill() + // - Update() and Revert() + // + // + // + // Examples: + // + // Install Google Maps on the device. + // device/apps.Install("/google.com/appstore/maps", nil, nil) --> "google maps/0" + // + // Create and start an instance of the previously installed maps application + // installation. + // device/apps/google maps/0.Instantiate() --> { "0" } + // device/apps/google maps/0/0.Run() + // + // Create and start a second instance of the previously installed maps + // application installation. + // device/apps/google maps/0.Instantiate() --> { "1" } + // device/apps/google maps/0/1.Run() + // + // Kill and delete the first instance previously started. + // device/apps/google maps/0/0.Kill() + // device/apps/google maps/0/0.Delete() + // + // Install a second Google Maps installation. + // device/apps.Install("/google.com/appstore/maps", nil, nil) --> "google maps/1" + // + // Update the second maps installation to the latest version available. + // device/apps/google maps/1.Update() + // + // Update the first maps installation to a specific version. + // device/apps/google maps/0.UpdateTo("/google.com/appstore/beta/maps") + // + // Finally, an application installation instance can be in one of three abstract + // states: 1) "does not exist/deleted", 2) "running", or 3) "not-running". The + // interface methods transition between these abstract states using the + // following state machine: + // + // apply(Instantiate(), "does not exist") = "not-running" + // apply(Run(), "not-running") = "running" + // apply(Kill(), "running") = "not-running" + // apply(Delete(), "not-running") = "deleted" + ApplicationServerMethods + // Tidyable specifies that a service can be tidied. + tidyable.TidyableServerMethods + // Describe generates a description of the device. + Describe(*context.T, rpc.ServerCall) (Description, error) + // IsRunnable checks if the device can execute the given binary. + IsRunnable(_ *context.T, _ rpc.ServerCall, description binary.Description) (bool, error) + // Reset resets the device. If the deadline is non-zero and the device + // in question is still running after the given deadline expired, + // reset of the device is enforced. + Reset(_ *context.T, _ rpc.ServerCall, deadline time.Duration) error + // AssociateAccount associates a local system account name with the provided + // Vanadium identities. It replaces the existing association if one already exists for that + // identity. Setting an AccountName to "" removes the association for each + // listed identity. + AssociateAccount(_ *context.T, _ rpc.ServerCall, identityNames []string, accountName string) error + // ListAssociations returns all of the associations between Vanadium identities + // and system names. + ListAssociations(*context.T, rpc.ServerCall) ([]Association, error) +} + +// DeviceServerStubMethods is the server interface containing +// Device methods, as expected by rpc.Server. +// The only difference between this interface and DeviceServerMethods +// is the streaming methods. +type DeviceServerStubMethods interface { + // Application can be used to manage applications on a device. This interface + // will be invoked using an object name that identifies the application and its + // installations and instances where applicable. + // + // An application is defined by a title. An application can have multiple + // installations on a device. The installations are grouped under the same + // application, but are otherwise independent of each other. Each installation + // can have zero or more instances (which can be running or not). The instances + // are independent of each other, and do not share state (like local storage). + // Interaction among instances should occur via Vanadium RPC, facilitated by the + // local mounttable. + // + // The device manager supports versioning of applications. Each installation + // maintains a tree of versions, where a version is defined by a specific + // envelope. The tree structure comes from 'previous version' references: each + // version (except the initial installation version) maintains a reference to + // the version that preceded it. The installation maintains a current version + // reference that is used for new instances. Each update operation on the + // installation creates a new version, sets the previous reference of the new + // version to the current version, and then updates the current version to refer + // to the new version. Each revert operation on the installation sets the + // current version to the previous version of the current version. Each + // instance maintains a current version reference that is used to run the + // instance. The initial version of the instance is set to the current version + // of the installation at the time of instantiation. Each update operation on + // the instance updates the instance's current version to the current version of + // the installation. Each revert operation on the instance updates the + // instance's current version to the previous version of the instance's version. + // + // The Application interface methods can be divided based on their intended + // receiver: + // + // 1) Method receiver is an application: + // - Install() + // + // 2) Method receiver is an application installation: + // - Instantiate() + // - Uninstall() + // + // 3) Method receiver is an application instance: + // - Run() + // - Kill() + // - Delete() + // + // 4) Method receiver is an application installation or instance: + // - Update() + // - Revert() + // + // The following methods complement one another: + // - Install() and Uninstall() + // - Instantiate() and Delete() + // - Run() and Kill() + // - Update() and Revert() + // + // + // + // Examples: + // + // Install Google Maps on the device. + // device/apps.Install("/google.com/appstore/maps", nil, nil) --> "google maps/0" + // + // Create and start an instance of the previously installed maps application + // installation. + // device/apps/google maps/0.Instantiate() --> { "0" } + // device/apps/google maps/0/0.Run() + // + // Create and start a second instance of the previously installed maps + // application installation. + // device/apps/google maps/0.Instantiate() --> { "1" } + // device/apps/google maps/0/1.Run() + // + // Kill and delete the first instance previously started. + // device/apps/google maps/0/0.Kill() + // device/apps/google maps/0/0.Delete() + // + // Install a second Google Maps installation. + // device/apps.Install("/google.com/appstore/maps", nil, nil) --> "google maps/1" + // + // Update the second maps installation to the latest version available. + // device/apps/google maps/1.Update() + // + // Update the first maps installation to a specific version. + // device/apps/google maps/0.UpdateTo("/google.com/appstore/beta/maps") + // + // Finally, an application installation instance can be in one of three abstract + // states: 1) "does not exist/deleted", 2) "running", or 3) "not-running". The + // interface methods transition between these abstract states using the + // following state machine: + // + // apply(Instantiate(), "does not exist") = "not-running" + // apply(Run(), "not-running") = "running" + // apply(Kill(), "running") = "not-running" + // apply(Delete(), "not-running") = "deleted" + ApplicationServerStubMethods + // Tidyable specifies that a service can be tidied. + tidyable.TidyableServerStubMethods + // Describe generates a description of the device. + Describe(*context.T, rpc.ServerCall) (Description, error) + // IsRunnable checks if the device can execute the given binary. + IsRunnable(_ *context.T, _ rpc.ServerCall, description binary.Description) (bool, error) + // Reset resets the device. If the deadline is non-zero and the device + // in question is still running after the given deadline expired, + // reset of the device is enforced. + Reset(_ *context.T, _ rpc.ServerCall, deadline time.Duration) error + // AssociateAccount associates a local system account name with the provided + // Vanadium identities. It replaces the existing association if one already exists for that + // identity. Setting an AccountName to "" removes the association for each + // listed identity. + AssociateAccount(_ *context.T, _ rpc.ServerCall, identityNames []string, accountName string) error + // ListAssociations returns all of the associations between Vanadium identities + // and system names. + ListAssociations(*context.T, rpc.ServerCall) ([]Association, error) +} + +// DeviceServerStub adds universal methods to DeviceServerStubMethods. +type DeviceServerStub interface { + DeviceServerStubMethods + // Describe the Device interfaces. + Describe__() []rpc.InterfaceDesc +} + +// DeviceServer returns a server stub for Device. +// It converts an implementation of DeviceServerMethods into +// an object that may be used by rpc.Server. +func DeviceServer(impl DeviceServerMethods) DeviceServerStub { + stub := implDeviceServerStub{ + impl: impl, + ApplicationServerStub: ApplicationServer(impl), + TidyableServerStub: tidyable.TidyableServer(impl), + } + // Initialize GlobState; always check the stub itself first, to handle the + // case where the user has the Glob method defined in their VDL source. + if gs := rpc.NewGlobState(stub); gs != nil { + stub.gs = gs + } else if gs := rpc.NewGlobState(impl); gs != nil { + stub.gs = gs + } + return stub +} + +type implDeviceServerStub struct { + impl DeviceServerMethods + ApplicationServerStub + tidyable.TidyableServerStub + gs *rpc.GlobState +} + +func (s implDeviceServerStub) Describe(ctx *context.T, call rpc.ServerCall) (Description, error) { + return s.impl.Describe(ctx, call) +} + +func (s implDeviceServerStub) IsRunnable(ctx *context.T, call rpc.ServerCall, i0 binary.Description) (bool, error) { + return s.impl.IsRunnable(ctx, call, i0) +} + +func (s implDeviceServerStub) Reset(ctx *context.T, call rpc.ServerCall, i0 time.Duration) error { + return s.impl.Reset(ctx, call, i0) +} + +func (s implDeviceServerStub) AssociateAccount(ctx *context.T, call rpc.ServerCall, i0 []string, i1 string) error { + return s.impl.AssociateAccount(ctx, call, i0, i1) +} + +func (s implDeviceServerStub) ListAssociations(ctx *context.T, call rpc.ServerCall) ([]Association, error) { + return s.impl.ListAssociations(ctx, call) +} + +func (s implDeviceServerStub) Globber() *rpc.GlobState { + return s.gs +} + +func (s implDeviceServerStub) Describe__() []rpc.InterfaceDesc { + return []rpc.InterfaceDesc{DeviceDesc, ApplicationDesc, permissions.ObjectDesc, tidyable.TidyableDesc} +} + +// DeviceDesc describes the Device interface. +var DeviceDesc rpc.InterfaceDesc = descDevice + +// descDevice hides the desc to keep godoc clean. +var descDevice = rpc.InterfaceDesc{ + Name: "Device", + PkgPath: "v.io/v23/services/device", + Doc: "// Device can be used to manage a device remotely using an object name that\n// identifies it.", + Embeds: []rpc.EmbedDesc{ + {"Application", "v.io/v23/services/device", "// Application can be used to manage applications on a device. This interface\n// will be invoked using an object name that identifies the application and its\n// installations and instances where applicable.\n//\n// An application is defined by a title. An application can have multiple\n// installations on a device. The installations are grouped under the same\n// application, but are otherwise independent of each other. Each installation\n// can have zero or more instances (which can be running or not). The instances\n// are independent of each other, and do not share state (like local storage).\n// Interaction among instances should occur via Vanadium RPC, facilitated by the\n// local mounttable.\n//\n// The device manager supports versioning of applications. Each installation\n// maintains a tree of versions, where a version is defined by a specific\n// envelope. The tree structure comes from 'previous version' references: each\n// version (except the initial installation version) maintains a reference to\n// the version that preceded it. The installation maintains a current version\n// reference that is used for new instances. Each update operation on the\n// installation creates a new version, sets the previous reference of the new\n// version to the current version, and then updates the current version to refer\n// to the new version. Each revert operation on the installation sets the\n// current version to the previous version of the current version. Each\n// instance maintains a current version reference that is used to run the\n// instance. The initial version of the instance is set to the current version\n// of the installation at the time of instantiation. Each update operation on\n// the instance updates the instance's current version to the current version of\n// the installation. Each revert operation on the instance updates the\n// instance's current version to the previous version of the instance's version.\n//\n// The Application interface methods can be divided based on their intended\n// receiver:\n//\n// 1) Method receiver is an application:\n// - Install()\n//\n// 2) Method receiver is an application installation:\n// - Instantiate()\n// - Uninstall()\n//\n// 3) Method receiver is an application instance:\n// - Run()\n// - Kill()\n// - Delete()\n//\n// 4) Method receiver is an application installation or instance:\n// - Update()\n// - Revert()\n//\n// The following methods complement one another:\n// - Install() and Uninstall()\n// - Instantiate() and Delete()\n// - Run() and Kill()\n// - Update() and Revert()\n//\n//\n//\n// Examples:\n//\n// Install Google Maps on the device.\n// device/apps.Install(\"/google.com/appstore/maps\", nil, nil) --> \"google maps/0\"\n//\n// Create and start an instance of the previously installed maps application\n// installation.\n// device/apps/google maps/0.Instantiate() --> { \"0\" }\n// device/apps/google maps/0/0.Run()\n//\n// Create and start a second instance of the previously installed maps\n// application installation.\n// device/apps/google maps/0.Instantiate() --> { \"1\" }\n// device/apps/google maps/0/1.Run()\n//\n// Kill and delete the first instance previously started.\n// device/apps/google maps/0/0.Kill()\n// device/apps/google maps/0/0.Delete()\n//\n// Install a second Google Maps installation.\n// device/apps.Install(\"/google.com/appstore/maps\", nil, nil) --> \"google maps/1\"\n//\n// Update the second maps installation to the latest version available.\n// device/apps/google maps/1.Update()\n//\n// Update the first maps installation to a specific version.\n// device/apps/google maps/0.UpdateTo(\"/google.com/appstore/beta/maps\")\n//\n// Finally, an application installation instance can be in one of three abstract\n// states: 1) \"does not exist/deleted\", 2) \"running\", or 3) \"not-running\". The\n// interface methods transition between these abstract states using the\n// following state machine:\n//\n// apply(Instantiate(), \"does not exist\") = \"not-running\"\n// apply(Run(), \"not-running\") = \"running\"\n// apply(Kill(), \"running\") = \"not-running\"\n// apply(Delete(), \"not-running\") = \"deleted\""}, + {"Tidyable", "v.io/v23/services/tidyable", "// Tidyable specifies that a service can be tidied."}, + }, + Methods: []rpc.MethodDesc{ + { + Name: "Describe", + Doc: "// Describe generates a description of the device.", + OutArgs: []rpc.ArgDesc{ + {"", ``}, // Description + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))}, + }, + { + Name: "IsRunnable", + Doc: "// IsRunnable checks if the device can execute the given binary.", + InArgs: []rpc.ArgDesc{ + {"description", ``}, // binary.Description + }, + OutArgs: []rpc.ArgDesc{ + {"", ``}, // bool + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))}, + }, + { + Name: "Reset", + Doc: "// Reset resets the device. If the deadline is non-zero and the device\n// in question is still running after the given deadline expired,\n// reset of the device is enforced.", + InArgs: []rpc.ArgDesc{ + {"deadline", ``}, // time.Duration + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))}, + }, + { + Name: "AssociateAccount", + Doc: "// AssociateAccount associates a local system account name with the provided\n// Vanadium identities. It replaces the existing association if one already exists for that\n// identity. Setting an AccountName to \"\" removes the association for each\n// listed identity.", + InArgs: []rpc.ArgDesc{ + {"identityNames", ``}, // []string + {"accountName", ``}, // string + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))}, + }, + { + Name: "ListAssociations", + Doc: "// ListAssociations returns all of the associations between Vanadium identities\n// and system names.", + OutArgs: []rpc.ArgDesc{ + {"", ``}, // []Association + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))}, + }, + }, +} + +// Hold type definitions in package-level variables, for better performance. +var ( + __VDLType_map_1 *vdl.Type + __VDLType_enum_2 *vdl.Type + __VDLType_enum_3 *vdl.Type + __VDLType_struct_4 *vdl.Type + __VDLType_struct_5 *vdl.Type + __VDLType_struct_6 *vdl.Type + __VDLType_union_7 *vdl.Type + __VDLType_list_8 *vdl.Type + __VDLType_union_9 *vdl.Type + __VDLType_struct_10 *vdl.Type + __VDLType_union_11 *vdl.Type + __VDLType_struct_12 *vdl.Type + __VDLType_set_13 *vdl.Type + __VDLType_struct_14 *vdl.Type +) + +var __VDLInitCalled bool + +// __VDLInit performs vdl initialization. It is safe to call multiple times. +// If you have an init ordering issue, just insert the following line verbatim +// into your source files in this package, right after the "package foo" clause: +// +// var _ = __VDLInit() +// +// The purpose of this function is to ensure that vdl initialization occurs in +// the right order, and very early in the init sequence. In particular, vdl +// registration and package variable initialization needs to occur before +// functions like vdl.TypeOf will work properly. +// +// This function returns a dummy value, so that it can be used to initialize the +// first var in the file, to take advantage of Go's defined init order. +func __VDLInit() struct{} { + if __VDLInitCalled { + return struct{}{} + } + __VDLInitCalled = true + + // Register types. + vdl.Register((*Config)(nil)) + vdl.Register((*InstallationState)(nil)) + vdl.Register((*InstanceState)(nil)) + vdl.Register((*InstanceStatus)(nil)) + vdl.Register((*InstallationStatus)(nil)) + vdl.Register((*DeviceStatus)(nil)) + vdl.Register((*Status)(nil)) + vdl.Register((*BlessServerMessage)(nil)) + vdl.Register((*BlessClientMessage)(nil)) + vdl.Register((*Description)(nil)) + vdl.Register((*Association)(nil)) + + // Initialize type definitions. + __VDLType_map_1 = vdl.TypeOf((*Config)(nil)) + __VDLType_enum_2 = vdl.TypeOf((*InstallationState)(nil)) + __VDLType_enum_3 = vdl.TypeOf((*InstanceState)(nil)) + __VDLType_struct_4 = vdl.TypeOf((*InstanceStatus)(nil)).Elem() + __VDLType_struct_5 = vdl.TypeOf((*InstallationStatus)(nil)).Elem() + __VDLType_struct_6 = vdl.TypeOf((*DeviceStatus)(nil)).Elem() + __VDLType_union_7 = vdl.TypeOf((*Status)(nil)) + __VDLType_list_8 = vdl.TypeOf((*[]byte)(nil)) + __VDLType_union_9 = vdl.TypeOf((*BlessServerMessage)(nil)) + __VDLType_struct_10 = vdl.TypeOf((*security.WireBlessings)(nil)).Elem() + __VDLType_union_11 = vdl.TypeOf((*BlessClientMessage)(nil)) + __VDLType_struct_12 = vdl.TypeOf((*Description)(nil)).Elem() + __VDLType_set_13 = vdl.TypeOf((*map[string]struct{})(nil)) + __VDLType_struct_14 = vdl.TypeOf((*Association)(nil)).Elem() + + return struct{}{} +} diff --git a/v23/services/repository/repository.vdl b/v23/services/repository/repository.vdl new file mode 100644 index 000000000..c666b396a --- /dev/null +++ b/v23/services/repository/repository.vdl @@ -0,0 +1,124 @@ +// Copyright 2015 The Vanadium 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 repository defines interfaces for storing and retrieving device, +// application and binary management related information. +package repository + +import ( + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/services/binary" + "v.io/v23/services/permissions" + "v.io/v23/services/tidyable" +) + +// Application provides access to application envelopes. An +// application envelope is identified by an application name and an +// application version, which are specified through the object name, +// and a profile name, which is specified using a method argument. +// +// Example: +// /apps/search/v1.Match([]string{"base", "media"}) +// returns an application envelope that can be used for downloading +// and executing the "search" application, version "v1", runnable +// on either the "base" or "media" profile. +type Application interface { + // Object provides GetPermissions/SetPermissions methods to read/modify + // Permissions for the Application methods. + permissions.Object + // Tidyable provides TidyNow to force cleanup of state now. + tidyable.Tidyable + // Match checks if any of the given profiles contains an application + // envelope for the given application version (specified through the + // object name suffix) and if so, returns this envelope. If multiple + // profile matches are possible, the method returns the first + // matching profile, respecting the order of the input argument. + // + // If the version is not specified in the suffix, the envelope + // corresponding to the latest version that matches any of the given + // profiles is returned. If several profiles match this version, the + // envelope for the first matching profile is returned, respecting the + // order of the input argument. + Match(profiles []string) (application.Envelope | error) {access.Read} +} + +// MediaInfo contains the metadata information for a binary. +type MediaInfo struct { + Type string // The media-type (RFC 2046) + Encoding string // The file encoding is optional and can be either "gzip" or "bzip2". +} + +// Binary can be used to store and retrieve vanadium application +// binaries. +// +// To create a binary, clients first invoke the Create() method that +// specifies the number of parts the binary consists of. Clients then +// uploads the individual parts through the Upload() method, which +// identifies the part being uploaded. To resume an upload after a +// failure, clients invoke the UploadStatus() method, which returns a +// slice that identifies which parts are missing. +// +// To download a binary, clients first invoke Stat(), which returns +// information describing the binary, including the number of parts +// the binary consists of. Clients then download the individual parts +// through the Download() method, which identifies the part being +// downloaded. Alternatively, clients can download the binary through +// HTTP using a transient URL available through the DownloadUrl() +// method. +// +// To delete the binary, clients invoke the Delete() method. +type Binary interface { + // Object provides GetPermissions/SetPermissions methods to read/modify + // Permissions for the Binary methods. + permissions.Object + // Create expresses the intent to create a binary identified by the + // object name suffix consisting of the given number of parts. The + // mediaInfo argument contains metadata for the binary. If the suffix + // identifies a binary that has already been created, the method + // returns an error. + Create(nparts int32, mediaInfo MediaInfo) error {access.Write} + // Delete deletes the binary identified by the object name + // suffix. If the binary that has not been created, the method + // returns an error. + Delete() error {access.Write} + // Download opens a stream that can used for downloading the given + // part of the binary identified by the object name suffix. If the + // binary part has not been uploaded, the method returns an + // error. If the Delete() method is invoked when the Download() + // method is in progress, the outcome the Download() method is + // undefined. + Download(part int32) stream<_, []byte> error {access.Read} + // DownloadUrl returns a transient URL from which the binary + // identified by the object name suffix can be downloaded using the + // HTTP protocol. If not all parts of the binary have been uploaded, + // the method returns an error. + DownloadUrl() (url string, ttl int64 | error) {access.Read} + // Stat returns information describing the parts of the binary + // identified by the object name suffix, and its RFC 2046 media type. + // If the binary has not been created, the method returns an error. + Stat() (Parts []binary.PartInfo, MediaInfo MediaInfo | error) {access.Read} + // Upload opens a stream that can be used for uploading the given + // part of the binary identified by the object name suffix. If the + // binary has not been created, the method returns an error. If the + // binary part has been uploaded, the method returns an error. If + // the same binary part is being uploaded by another caller, the + // method returns an error. + Upload(part int32) stream<[]byte> error {access.Write} +} + +// Profile abstracts a device's ability to run binaries, and hides +// specifics such as the operating system, hardware architecture, and +// the set of installed libraries. Profiles describe binaries and +// devices, and are used to match them. +type Profile interface { + // Label is the human-readable profile key for the profile, + // e.g. "linux-media". The label can be used to uniquely identify + // the profile (for the purpose of matching application binaries and + // devices). + Label() (string | error) {access.Read} + // Description is a free-text description of the profile, meant for + // human consumption. + Description() (string | error) {access.Read} +} diff --git a/v23/services/repository/repository.vdl.go b/v23/services/repository/repository.vdl.go new file mode 100644 index 000000000..dbd6548a2 --- /dev/null +++ b/v23/services/repository/repository.vdl.go @@ -0,0 +1,1242 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated by the vanadium vdl tool. +// Package: repository + +// Package repository defines interfaces for storing and retrieving device, +// application and binary management related information. +package repository + +import ( + "io" + "v.io/v23" + "v.io/v23/context" + "v.io/v23/rpc" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/services/binary" + "v.io/v23/services/permissions" + "v.io/v23/services/tidyable" + "v.io/v23/vdl" +) + +var _ = __VDLInit() // Must be first; see __VDLInit comments for details. + +////////////////////////////////////////////////// +// Type definitions + +// MediaInfo contains the metadata information for a binary. +type MediaInfo struct { + Type string // The media-type (RFC 2046) + Encoding string // The file encoding is optional and can be either "gzip" or "bzip2". +} + +func (MediaInfo) VDLReflect(struct { + Name string `vdl:"v.io/v23/services/repository.MediaInfo"` +}) { +} + +func (x MediaInfo) VDLIsZero() bool { + return x == MediaInfo{} +} + +func (x MediaInfo) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_1); err != nil { + return err + } + if x.Type != "" { + if err := enc.NextFieldValueString(0, vdl.StringType, x.Type); err != nil { + return err + } + } + if x.Encoding != "" { + if err := enc.NextFieldValueString(1, vdl.StringType, x.Encoding); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *MediaInfo) VDLRead(dec vdl.Decoder) error { + *x = MediaInfo{} + if err := dec.StartValue(__VDLType_struct_1); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_1 { + index = __VDLType_struct_1.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.Type = value + } + case 1: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.Encoding = value + } + } + } +} + +////////////////////////////////////////////////// +// Interface definitions + +// ApplicationClientMethods is the client interface +// containing Application methods. +// +// Application provides access to application envelopes. An +// application envelope is identified by an application name and an +// application version, which are specified through the object name, +// and a profile name, which is specified using a method argument. +// +// Example: +// /apps/search/v1.Match([]string{"base", "media"}) +// returns an application envelope that can be used for downloading +// and executing the "search" application, version "v1", runnable +// on either the "base" or "media" profile. +type ApplicationClientMethods interface { + // Object provides access control for Vanadium objects. + // + // Vanadium services implementing dynamic access control would typically embed + // this interface and tag additional methods defined by the service with one of + // Admin, Read, Write, Resolve etc. For example, the VDL definition of the + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // import "v.io/v23/services/permissions" + // + // type MyObject interface { + // permissions.Object + // MyRead() (string, error) {access.Read} + // MyWrite(string) error {access.Write} + // } + // + // If the set of pre-defined tags is insufficient, services may define their + // own tag type and annotate all methods with this new type. + // + // Instead of embedding this Object interface, define SetPermissions and + // GetPermissions in their own interface. Authorization policies will typically + // respect annotations of a single type. For example, the VDL definition of an + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // + // type MyTag string + // + // const ( + // Blue = MyTag("Blue") + // Red = MyTag("Red") + // ) + // + // type MyObject interface { + // MyMethod() (string, error) {Blue} + // + // // Allow clients to change access via the access.Object interface: + // SetPermissions(perms access.Permissions, version string) error {Red} + // GetPermissions() (perms access.Permissions, version string, err error) {Blue} + // } + permissions.ObjectClientMethods + // Tidyable specifies that a service can be tidied. + tidyable.TidyableClientMethods + // Match checks if any of the given profiles contains an application + // envelope for the given application version (specified through the + // object name suffix) and if so, returns this envelope. If multiple + // profile matches are possible, the method returns the first + // matching profile, respecting the order of the input argument. + // + // If the version is not specified in the suffix, the envelope + // corresponding to the latest version that matches any of the given + // profiles is returned. If several profiles match this version, the + // envelope for the first matching profile is returned, respecting the + // order of the input argument. + Match(_ *context.T, profiles []string, _ ...rpc.CallOpt) (application.Envelope, error) +} + +// ApplicationClientStub adds universal methods to ApplicationClientMethods. +type ApplicationClientStub interface { + ApplicationClientMethods + rpc.UniversalServiceMethods +} + +// ApplicationClient returns a client stub for Application. +func ApplicationClient(name string) ApplicationClientStub { + return implApplicationClientStub{name, permissions.ObjectClient(name), tidyable.TidyableClient(name)} +} + +type implApplicationClientStub struct { + name string + + permissions.ObjectClientStub + tidyable.TidyableClientStub +} + +func (c implApplicationClientStub) Match(ctx *context.T, i0 []string, opts ...rpc.CallOpt) (o0 application.Envelope, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Match", []interface{}{i0}, []interface{}{&o0}, opts...) + return +} + +// ApplicationServerMethods is the interface a server writer +// implements for Application. +// +// Application provides access to application envelopes. An +// application envelope is identified by an application name and an +// application version, which are specified through the object name, +// and a profile name, which is specified using a method argument. +// +// Example: +// /apps/search/v1.Match([]string{"base", "media"}) +// returns an application envelope that can be used for downloading +// and executing the "search" application, version "v1", runnable +// on either the "base" or "media" profile. +type ApplicationServerMethods interface { + // Object provides access control for Vanadium objects. + // + // Vanadium services implementing dynamic access control would typically embed + // this interface and tag additional methods defined by the service with one of + // Admin, Read, Write, Resolve etc. For example, the VDL definition of the + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // import "v.io/v23/services/permissions" + // + // type MyObject interface { + // permissions.Object + // MyRead() (string, error) {access.Read} + // MyWrite(string) error {access.Write} + // } + // + // If the set of pre-defined tags is insufficient, services may define their + // own tag type and annotate all methods with this new type. + // + // Instead of embedding this Object interface, define SetPermissions and + // GetPermissions in their own interface. Authorization policies will typically + // respect annotations of a single type. For example, the VDL definition of an + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // + // type MyTag string + // + // const ( + // Blue = MyTag("Blue") + // Red = MyTag("Red") + // ) + // + // type MyObject interface { + // MyMethod() (string, error) {Blue} + // + // // Allow clients to change access via the access.Object interface: + // SetPermissions(perms access.Permissions, version string) error {Red} + // GetPermissions() (perms access.Permissions, version string, err error) {Blue} + // } + permissions.ObjectServerMethods + // Tidyable specifies that a service can be tidied. + tidyable.TidyableServerMethods + // Match checks if any of the given profiles contains an application + // envelope for the given application version (specified through the + // object name suffix) and if so, returns this envelope. If multiple + // profile matches are possible, the method returns the first + // matching profile, respecting the order of the input argument. + // + // If the version is not specified in the suffix, the envelope + // corresponding to the latest version that matches any of the given + // profiles is returned. If several profiles match this version, the + // envelope for the first matching profile is returned, respecting the + // order of the input argument. + Match(_ *context.T, _ rpc.ServerCall, profiles []string) (application.Envelope, error) +} + +// ApplicationServerStubMethods is the server interface containing +// Application methods, as expected by rpc.Server. +// There is no difference between this interface and ApplicationServerMethods +// since there are no streaming methods. +type ApplicationServerStubMethods ApplicationServerMethods + +// ApplicationServerStub adds universal methods to ApplicationServerStubMethods. +type ApplicationServerStub interface { + ApplicationServerStubMethods + // Describe the Application interfaces. + Describe__() []rpc.InterfaceDesc +} + +// ApplicationServer returns a server stub for Application. +// It converts an implementation of ApplicationServerMethods into +// an object that may be used by rpc.Server. +func ApplicationServer(impl ApplicationServerMethods) ApplicationServerStub { + stub := implApplicationServerStub{ + impl: impl, + ObjectServerStub: permissions.ObjectServer(impl), + TidyableServerStub: tidyable.TidyableServer(impl), + } + // Initialize GlobState; always check the stub itself first, to handle the + // case where the user has the Glob method defined in their VDL source. + if gs := rpc.NewGlobState(stub); gs != nil { + stub.gs = gs + } else if gs := rpc.NewGlobState(impl); gs != nil { + stub.gs = gs + } + return stub +} + +type implApplicationServerStub struct { + impl ApplicationServerMethods + permissions.ObjectServerStub + tidyable.TidyableServerStub + gs *rpc.GlobState +} + +func (s implApplicationServerStub) Match(ctx *context.T, call rpc.ServerCall, i0 []string) (application.Envelope, error) { + return s.impl.Match(ctx, call, i0) +} + +func (s implApplicationServerStub) Globber() *rpc.GlobState { + return s.gs +} + +func (s implApplicationServerStub) Describe__() []rpc.InterfaceDesc { + return []rpc.InterfaceDesc{ApplicationDesc, permissions.ObjectDesc, tidyable.TidyableDesc} +} + +// ApplicationDesc describes the Application interface. +var ApplicationDesc rpc.InterfaceDesc = descApplication + +// descApplication hides the desc to keep godoc clean. +var descApplication = rpc.InterfaceDesc{ + Name: "Application", + PkgPath: "v.io/v23/services/repository", + Doc: "// Application provides access to application envelopes. An\n// application envelope is identified by an application name and an\n// application version, which are specified through the object name,\n// and a profile name, which is specified using a method argument.\n//\n// Example:\n// /apps/search/v1.Match([]string{\"base\", \"media\"})\n// returns an application envelope that can be used for downloading\n// and executing the \"search\" application, version \"v1\", runnable\n// on either the \"base\" or \"media\" profile.", + Embeds: []rpc.EmbedDesc{ + {"Object", "v.io/v23/services/permissions", "// Object provides access control for Vanadium objects.\n//\n// Vanadium services implementing dynamic access control would typically embed\n// this interface and tag additional methods defined by the service with one of\n// Admin, Read, Write, Resolve etc. For example, the VDL definition of the\n// object would be:\n//\n// package mypackage\n//\n// import \"v.io/v23/security/access\"\n// import \"v.io/v23/services/permissions\"\n//\n// type MyObject interface {\n// permissions.Object\n// MyRead() (string, error) {access.Read}\n// MyWrite(string) error {access.Write}\n// }\n//\n// If the set of pre-defined tags is insufficient, services may define their\n// own tag type and annotate all methods with this new type.\n//\n// Instead of embedding this Object interface, define SetPermissions and\n// GetPermissions in their own interface. Authorization policies will typically\n// respect annotations of a single type. For example, the VDL definition of an\n// object would be:\n//\n// package mypackage\n//\n// import \"v.io/v23/security/access\"\n//\n// type MyTag string\n//\n// const (\n// Blue = MyTag(\"Blue\")\n// Red = MyTag(\"Red\")\n// )\n//\n// type MyObject interface {\n// MyMethod() (string, error) {Blue}\n//\n// // Allow clients to change access via the access.Object interface:\n// SetPermissions(perms access.Permissions, version string) error {Red}\n// GetPermissions() (perms access.Permissions, version string, err error) {Blue}\n// }"}, + {"Tidyable", "v.io/v23/services/tidyable", "// Tidyable specifies that a service can be tidied."}, + }, + Methods: []rpc.MethodDesc{ + { + Name: "Match", + Doc: "// Match checks if any of the given profiles contains an application\n// envelope for the given application version (specified through the\n// object name suffix) and if so, returns this envelope. If multiple\n// profile matches are possible, the method returns the first\n// matching profile, respecting the order of the input argument.\n//\n// If the version is not specified in the suffix, the envelope\n// corresponding to the latest version that matches any of the given\n// profiles is returned. If several profiles match this version, the\n// envelope for the first matching profile is returned, respecting the\n// order of the input argument.", + InArgs: []rpc.ArgDesc{ + {"profiles", ``}, // []string + }, + OutArgs: []rpc.ArgDesc{ + {"", ``}, // application.Envelope + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))}, + }, + }, +} + +// BinaryClientMethods is the client interface +// containing Binary methods. +// +// Binary can be used to store and retrieve vanadium application +// binaries. +// +// To create a binary, clients first invoke the Create() method that +// specifies the number of parts the binary consists of. Clients then +// uploads the individual parts through the Upload() method, which +// identifies the part being uploaded. To resume an upload after a +// failure, clients invoke the UploadStatus() method, which returns a +// slice that identifies which parts are missing. +// +// To download a binary, clients first invoke Stat(), which returns +// information describing the binary, including the number of parts +// the binary consists of. Clients then download the individual parts +// through the Download() method, which identifies the part being +// downloaded. Alternatively, clients can download the binary through +// HTTP using a transient URL available through the DownloadUrl() +// method. +// +// To delete the binary, clients invoke the Delete() method. +type BinaryClientMethods interface { + // Object provides access control for Vanadium objects. + // + // Vanadium services implementing dynamic access control would typically embed + // this interface and tag additional methods defined by the service with one of + // Admin, Read, Write, Resolve etc. For example, the VDL definition of the + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // import "v.io/v23/services/permissions" + // + // type MyObject interface { + // permissions.Object + // MyRead() (string, error) {access.Read} + // MyWrite(string) error {access.Write} + // } + // + // If the set of pre-defined tags is insufficient, services may define their + // own tag type and annotate all methods with this new type. + // + // Instead of embedding this Object interface, define SetPermissions and + // GetPermissions in their own interface. Authorization policies will typically + // respect annotations of a single type. For example, the VDL definition of an + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // + // type MyTag string + // + // const ( + // Blue = MyTag("Blue") + // Red = MyTag("Red") + // ) + // + // type MyObject interface { + // MyMethod() (string, error) {Blue} + // + // // Allow clients to change access via the access.Object interface: + // SetPermissions(perms access.Permissions, version string) error {Red} + // GetPermissions() (perms access.Permissions, version string, err error) {Blue} + // } + permissions.ObjectClientMethods + // Create expresses the intent to create a binary identified by the + // object name suffix consisting of the given number of parts. The + // mediaInfo argument contains metadata for the binary. If the suffix + // identifies a binary that has already been created, the method + // returns an error. + Create(_ *context.T, nparts int32, mediaInfo MediaInfo, _ ...rpc.CallOpt) error + // Delete deletes the binary identified by the object name + // suffix. If the binary that has not been created, the method + // returns an error. + Delete(*context.T, ...rpc.CallOpt) error + // Download opens a stream that can used for downloading the given + // part of the binary identified by the object name suffix. If the + // binary part has not been uploaded, the method returns an + // error. If the Delete() method is invoked when the Download() + // method is in progress, the outcome the Download() method is + // undefined. + Download(_ *context.T, part int32, _ ...rpc.CallOpt) (BinaryDownloadClientCall, error) + // DownloadUrl returns a transient URL from which the binary + // identified by the object name suffix can be downloaded using the + // HTTP protocol. If not all parts of the binary have been uploaded, + // the method returns an error. + DownloadUrl(*context.T, ...rpc.CallOpt) (url string, ttl int64, _ error) + // Stat returns information describing the parts of the binary + // identified by the object name suffix, and its RFC 2046 media type. + // If the binary has not been created, the method returns an error. + Stat(*context.T, ...rpc.CallOpt) (Parts []binary.PartInfo, MediaInfo MediaInfo, _ error) + // Upload opens a stream that can be used for uploading the given + // part of the binary identified by the object name suffix. If the + // binary has not been created, the method returns an error. If the + // binary part has been uploaded, the method returns an error. If + // the same binary part is being uploaded by another caller, the + // method returns an error. + Upload(_ *context.T, part int32, _ ...rpc.CallOpt) (BinaryUploadClientCall, error) +} + +// BinaryClientStub adds universal methods to BinaryClientMethods. +type BinaryClientStub interface { + BinaryClientMethods + rpc.UniversalServiceMethods +} + +// BinaryClient returns a client stub for Binary. +func BinaryClient(name string) BinaryClientStub { + return implBinaryClientStub{name, permissions.ObjectClient(name)} +} + +type implBinaryClientStub struct { + name string + + permissions.ObjectClientStub +} + +func (c implBinaryClientStub) Create(ctx *context.T, i0 int32, i1 MediaInfo, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Create", []interface{}{i0, i1}, nil, opts...) + return +} + +func (c implBinaryClientStub) Delete(ctx *context.T, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Delete", nil, nil, opts...) + return +} + +func (c implBinaryClientStub) Download(ctx *context.T, i0 int32, opts ...rpc.CallOpt) (ocall BinaryDownloadClientCall, err error) { + var call rpc.ClientCall + if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "Download", []interface{}{i0}, opts...); err != nil { + return + } + ocall = &implBinaryDownloadClientCall{ClientCall: call} + return +} + +func (c implBinaryClientStub) DownloadUrl(ctx *context.T, opts ...rpc.CallOpt) (o0 string, o1 int64, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "DownloadUrl", nil, []interface{}{&o0, &o1}, opts...) + return +} + +func (c implBinaryClientStub) Stat(ctx *context.T, opts ...rpc.CallOpt) (o0 []binary.PartInfo, o1 MediaInfo, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Stat", nil, []interface{}{&o0, &o1}, opts...) + return +} + +func (c implBinaryClientStub) Upload(ctx *context.T, i0 int32, opts ...rpc.CallOpt) (ocall BinaryUploadClientCall, err error) { + var call rpc.ClientCall + if call, err = v23.GetClient(ctx).StartCall(ctx, c.name, "Upload", []interface{}{i0}, opts...); err != nil { + return + } + ocall = &implBinaryUploadClientCall{ClientCall: call} + return +} + +// BinaryDownloadClientStream is the client stream for Binary.Download. +type BinaryDownloadClientStream interface { + // RecvStream returns the receiver side of the Binary.Download client stream. + RecvStream() interface { + // Advance stages an item so that it may be retrieved via Value. Returns + // true iff there is an item to retrieve. Advance must be called before + // Value is called. May block if an item is not available. + Advance() bool + // Value returns the item that was staged by Advance. May panic if Advance + // returned false or was not called. Never blocks. + Value() []byte + // Err returns any error encountered by Advance. Never blocks. + Err() error + } +} + +// BinaryDownloadClientCall represents the call returned from Binary.Download. +type BinaryDownloadClientCall interface { + BinaryDownloadClientStream + // Finish blocks until the server is done, and returns the positional return + // values for call. + // + // Finish returns immediately if the call has been canceled; depending on the + // timing the output could either be an error signaling cancelation, or the + // valid positional return values from the server. + // + // Calling Finish is mandatory for releasing stream resources, unless the call + // has been canceled or any of the other methods return an error. Finish should + // be called at most once. + Finish() error +} + +type implBinaryDownloadClientCall struct { + rpc.ClientCall + valRecv []byte + errRecv error +} + +func (c *implBinaryDownloadClientCall) RecvStream() interface { + Advance() bool + Value() []byte + Err() error +} { + return implBinaryDownloadClientCallRecv{c} +} + +type implBinaryDownloadClientCallRecv struct { + c *implBinaryDownloadClientCall +} + +func (c implBinaryDownloadClientCallRecv) Advance() bool { + c.c.errRecv = c.c.Recv(&c.c.valRecv) + return c.c.errRecv == nil +} +func (c implBinaryDownloadClientCallRecv) Value() []byte { + return c.c.valRecv +} +func (c implBinaryDownloadClientCallRecv) Err() error { + if c.c.errRecv == io.EOF { + return nil + } + return c.c.errRecv +} +func (c *implBinaryDownloadClientCall) Finish() (err error) { + err = c.ClientCall.Finish() + return +} + +// BinaryUploadClientStream is the client stream for Binary.Upload. +type BinaryUploadClientStream interface { + // SendStream returns the send side of the Binary.Upload client stream. + SendStream() interface { + // Send places the item onto the output stream. Returns errors + // encountered while sending, or if Send is called after Close or + // the stream has been canceled. Blocks if there is no buffer + // space; will unblock when buffer space is available or after + // the stream has been canceled. + Send(item []byte) error + // Close indicates to the server that no more items will be sent; + // server Recv calls will receive io.EOF after all sent items. + // This is an optional call - e.g. a client might call Close if it + // needs to continue receiving items from the server after it's + // done sending. Returns errors encountered while closing, or if + // Close is called after the stream has been canceled. Like Send, + // blocks if there is no buffer space available. + Close() error + } +} + +// BinaryUploadClientCall represents the call returned from Binary.Upload. +type BinaryUploadClientCall interface { + BinaryUploadClientStream + // Finish performs the equivalent of SendStream().Close, then blocks until + // the server is done, and returns the positional return values for the call. + // + // Finish returns immediately if the call has been canceled; depending on the + // timing the output could either be an error signaling cancelation, or the + // valid positional return values from the server. + // + // Calling Finish is mandatory for releasing stream resources, unless the call + // has been canceled or any of the other methods return an error. Finish should + // be called at most once. + Finish() error +} + +type implBinaryUploadClientCall struct { + rpc.ClientCall +} + +func (c *implBinaryUploadClientCall) SendStream() interface { + Send(item []byte) error + Close() error +} { + return implBinaryUploadClientCallSend{c} +} + +type implBinaryUploadClientCallSend struct { + c *implBinaryUploadClientCall +} + +func (c implBinaryUploadClientCallSend) Send(item []byte) error { + return c.c.Send(item) +} +func (c implBinaryUploadClientCallSend) Close() error { + return c.c.CloseSend() +} +func (c *implBinaryUploadClientCall) Finish() (err error) { + err = c.ClientCall.Finish() + return +} + +// BinaryServerMethods is the interface a server writer +// implements for Binary. +// +// Binary can be used to store and retrieve vanadium application +// binaries. +// +// To create a binary, clients first invoke the Create() method that +// specifies the number of parts the binary consists of. Clients then +// uploads the individual parts through the Upload() method, which +// identifies the part being uploaded. To resume an upload after a +// failure, clients invoke the UploadStatus() method, which returns a +// slice that identifies which parts are missing. +// +// To download a binary, clients first invoke Stat(), which returns +// information describing the binary, including the number of parts +// the binary consists of. Clients then download the individual parts +// through the Download() method, which identifies the part being +// downloaded. Alternatively, clients can download the binary through +// HTTP using a transient URL available through the DownloadUrl() +// method. +// +// To delete the binary, clients invoke the Delete() method. +type BinaryServerMethods interface { + // Object provides access control for Vanadium objects. + // + // Vanadium services implementing dynamic access control would typically embed + // this interface and tag additional methods defined by the service with one of + // Admin, Read, Write, Resolve etc. For example, the VDL definition of the + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // import "v.io/v23/services/permissions" + // + // type MyObject interface { + // permissions.Object + // MyRead() (string, error) {access.Read} + // MyWrite(string) error {access.Write} + // } + // + // If the set of pre-defined tags is insufficient, services may define their + // own tag type and annotate all methods with this new type. + // + // Instead of embedding this Object interface, define SetPermissions and + // GetPermissions in their own interface. Authorization policies will typically + // respect annotations of a single type. For example, the VDL definition of an + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // + // type MyTag string + // + // const ( + // Blue = MyTag("Blue") + // Red = MyTag("Red") + // ) + // + // type MyObject interface { + // MyMethod() (string, error) {Blue} + // + // // Allow clients to change access via the access.Object interface: + // SetPermissions(perms access.Permissions, version string) error {Red} + // GetPermissions() (perms access.Permissions, version string, err error) {Blue} + // } + permissions.ObjectServerMethods + // Create expresses the intent to create a binary identified by the + // object name suffix consisting of the given number of parts. The + // mediaInfo argument contains metadata for the binary. If the suffix + // identifies a binary that has already been created, the method + // returns an error. + Create(_ *context.T, _ rpc.ServerCall, nparts int32, mediaInfo MediaInfo) error + // Delete deletes the binary identified by the object name + // suffix. If the binary that has not been created, the method + // returns an error. + Delete(*context.T, rpc.ServerCall) error + // Download opens a stream that can used for downloading the given + // part of the binary identified by the object name suffix. If the + // binary part has not been uploaded, the method returns an + // error. If the Delete() method is invoked when the Download() + // method is in progress, the outcome the Download() method is + // undefined. + Download(_ *context.T, _ BinaryDownloadServerCall, part int32) error + // DownloadUrl returns a transient URL from which the binary + // identified by the object name suffix can be downloaded using the + // HTTP protocol. If not all parts of the binary have been uploaded, + // the method returns an error. + DownloadUrl(*context.T, rpc.ServerCall) (url string, ttl int64, _ error) + // Stat returns information describing the parts of the binary + // identified by the object name suffix, and its RFC 2046 media type. + // If the binary has not been created, the method returns an error. + Stat(*context.T, rpc.ServerCall) (Parts []binary.PartInfo, MediaInfo MediaInfo, _ error) + // Upload opens a stream that can be used for uploading the given + // part of the binary identified by the object name suffix. If the + // binary has not been created, the method returns an error. If the + // binary part has been uploaded, the method returns an error. If + // the same binary part is being uploaded by another caller, the + // method returns an error. + Upload(_ *context.T, _ BinaryUploadServerCall, part int32) error +} + +// BinaryServerStubMethods is the server interface containing +// Binary methods, as expected by rpc.Server. +// The only difference between this interface and BinaryServerMethods +// is the streaming methods. +type BinaryServerStubMethods interface { + // Object provides access control for Vanadium objects. + // + // Vanadium services implementing dynamic access control would typically embed + // this interface and tag additional methods defined by the service with one of + // Admin, Read, Write, Resolve etc. For example, the VDL definition of the + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // import "v.io/v23/services/permissions" + // + // type MyObject interface { + // permissions.Object + // MyRead() (string, error) {access.Read} + // MyWrite(string) error {access.Write} + // } + // + // If the set of pre-defined tags is insufficient, services may define their + // own tag type and annotate all methods with this new type. + // + // Instead of embedding this Object interface, define SetPermissions and + // GetPermissions in their own interface. Authorization policies will typically + // respect annotations of a single type. For example, the VDL definition of an + // object would be: + // + // package mypackage + // + // import "v.io/v23/security/access" + // + // type MyTag string + // + // const ( + // Blue = MyTag("Blue") + // Red = MyTag("Red") + // ) + // + // type MyObject interface { + // MyMethod() (string, error) {Blue} + // + // // Allow clients to change access via the access.Object interface: + // SetPermissions(perms access.Permissions, version string) error {Red} + // GetPermissions() (perms access.Permissions, version string, err error) {Blue} + // } + permissions.ObjectServerStubMethods + // Create expresses the intent to create a binary identified by the + // object name suffix consisting of the given number of parts. The + // mediaInfo argument contains metadata for the binary. If the suffix + // identifies a binary that has already been created, the method + // returns an error. + Create(_ *context.T, _ rpc.ServerCall, nparts int32, mediaInfo MediaInfo) error + // Delete deletes the binary identified by the object name + // suffix. If the binary that has not been created, the method + // returns an error. + Delete(*context.T, rpc.ServerCall) error + // Download opens a stream that can used for downloading the given + // part of the binary identified by the object name suffix. If the + // binary part has not been uploaded, the method returns an + // error. If the Delete() method is invoked when the Download() + // method is in progress, the outcome the Download() method is + // undefined. + Download(_ *context.T, _ *BinaryDownloadServerCallStub, part int32) error + // DownloadUrl returns a transient URL from which the binary + // identified by the object name suffix can be downloaded using the + // HTTP protocol. If not all parts of the binary have been uploaded, + // the method returns an error. + DownloadUrl(*context.T, rpc.ServerCall) (url string, ttl int64, _ error) + // Stat returns information describing the parts of the binary + // identified by the object name suffix, and its RFC 2046 media type. + // If the binary has not been created, the method returns an error. + Stat(*context.T, rpc.ServerCall) (Parts []binary.PartInfo, MediaInfo MediaInfo, _ error) + // Upload opens a stream that can be used for uploading the given + // part of the binary identified by the object name suffix. If the + // binary has not been created, the method returns an error. If the + // binary part has been uploaded, the method returns an error. If + // the same binary part is being uploaded by another caller, the + // method returns an error. + Upload(_ *context.T, _ *BinaryUploadServerCallStub, part int32) error +} + +// BinaryServerStub adds universal methods to BinaryServerStubMethods. +type BinaryServerStub interface { + BinaryServerStubMethods + // Describe the Binary interfaces. + Describe__() []rpc.InterfaceDesc +} + +// BinaryServer returns a server stub for Binary. +// It converts an implementation of BinaryServerMethods into +// an object that may be used by rpc.Server. +func BinaryServer(impl BinaryServerMethods) BinaryServerStub { + stub := implBinaryServerStub{ + impl: impl, + ObjectServerStub: permissions.ObjectServer(impl), + } + // Initialize GlobState; always check the stub itself first, to handle the + // case where the user has the Glob method defined in their VDL source. + if gs := rpc.NewGlobState(stub); gs != nil { + stub.gs = gs + } else if gs := rpc.NewGlobState(impl); gs != nil { + stub.gs = gs + } + return stub +} + +type implBinaryServerStub struct { + impl BinaryServerMethods + permissions.ObjectServerStub + gs *rpc.GlobState +} + +func (s implBinaryServerStub) Create(ctx *context.T, call rpc.ServerCall, i0 int32, i1 MediaInfo) error { + return s.impl.Create(ctx, call, i0, i1) +} + +func (s implBinaryServerStub) Delete(ctx *context.T, call rpc.ServerCall) error { + return s.impl.Delete(ctx, call) +} + +func (s implBinaryServerStub) Download(ctx *context.T, call *BinaryDownloadServerCallStub, i0 int32) error { + return s.impl.Download(ctx, call, i0) +} + +func (s implBinaryServerStub) DownloadUrl(ctx *context.T, call rpc.ServerCall) (string, int64, error) { + return s.impl.DownloadUrl(ctx, call) +} + +func (s implBinaryServerStub) Stat(ctx *context.T, call rpc.ServerCall) ([]binary.PartInfo, MediaInfo, error) { + return s.impl.Stat(ctx, call) +} + +func (s implBinaryServerStub) Upload(ctx *context.T, call *BinaryUploadServerCallStub, i0 int32) error { + return s.impl.Upload(ctx, call, i0) +} + +func (s implBinaryServerStub) Globber() *rpc.GlobState { + return s.gs +} + +func (s implBinaryServerStub) Describe__() []rpc.InterfaceDesc { + return []rpc.InterfaceDesc{BinaryDesc, permissions.ObjectDesc} +} + +// BinaryDesc describes the Binary interface. +var BinaryDesc rpc.InterfaceDesc = descBinary + +// descBinary hides the desc to keep godoc clean. +var descBinary = rpc.InterfaceDesc{ + Name: "Binary", + PkgPath: "v.io/v23/services/repository", + Doc: "// Binary can be used to store and retrieve vanadium application\n// binaries.\n//\n// To create a binary, clients first invoke the Create() method that\n// specifies the number of parts the binary consists of. Clients then\n// uploads the individual parts through the Upload() method, which\n// identifies the part being uploaded. To resume an upload after a\n// failure, clients invoke the UploadStatus() method, which returns a\n// slice that identifies which parts are missing.\n//\n// To download a binary, clients first invoke Stat(), which returns\n// information describing the binary, including the number of parts\n// the binary consists of. Clients then download the individual parts\n// through the Download() method, which identifies the part being\n// downloaded. Alternatively, clients can download the binary through\n// HTTP using a transient URL available through the DownloadUrl()\n// method.\n//\n// To delete the binary, clients invoke the Delete() method.", + Embeds: []rpc.EmbedDesc{ + {"Object", "v.io/v23/services/permissions", "// Object provides access control for Vanadium objects.\n//\n// Vanadium services implementing dynamic access control would typically embed\n// this interface and tag additional methods defined by the service with one of\n// Admin, Read, Write, Resolve etc. For example, the VDL definition of the\n// object would be:\n//\n// package mypackage\n//\n// import \"v.io/v23/security/access\"\n// import \"v.io/v23/services/permissions\"\n//\n// type MyObject interface {\n// permissions.Object\n// MyRead() (string, error) {access.Read}\n// MyWrite(string) error {access.Write}\n// }\n//\n// If the set of pre-defined tags is insufficient, services may define their\n// own tag type and annotate all methods with this new type.\n//\n// Instead of embedding this Object interface, define SetPermissions and\n// GetPermissions in their own interface. Authorization policies will typically\n// respect annotations of a single type. For example, the VDL definition of an\n// object would be:\n//\n// package mypackage\n//\n// import \"v.io/v23/security/access\"\n//\n// type MyTag string\n//\n// const (\n// Blue = MyTag(\"Blue\")\n// Red = MyTag(\"Red\")\n// )\n//\n// type MyObject interface {\n// MyMethod() (string, error) {Blue}\n//\n// // Allow clients to change access via the access.Object interface:\n// SetPermissions(perms access.Permissions, version string) error {Red}\n// GetPermissions() (perms access.Permissions, version string, err error) {Blue}\n// }"}, + }, + Methods: []rpc.MethodDesc{ + { + Name: "Create", + Doc: "// Create expresses the intent to create a binary identified by the\n// object name suffix consisting of the given number of parts. The\n// mediaInfo argument contains metadata for the binary. If the suffix\n// identifies a binary that has already been created, the method\n// returns an error.", + InArgs: []rpc.ArgDesc{ + {"nparts", ``}, // int32 + {"mediaInfo", ``}, // MediaInfo + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))}, + }, + { + Name: "Delete", + Doc: "// Delete deletes the binary identified by the object name\n// suffix. If the binary that has not been created, the method\n// returns an error.", + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))}, + }, + { + Name: "Download", + Doc: "// Download opens a stream that can used for downloading the given\n// part of the binary identified by the object name suffix. If the\n// binary part has not been uploaded, the method returns an\n// error. If the Delete() method is invoked when the Download()\n// method is in progress, the outcome the Download() method is\n// undefined.", + InArgs: []rpc.ArgDesc{ + {"part", ``}, // int32 + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))}, + }, + { + Name: "DownloadUrl", + Doc: "// DownloadUrl returns a transient URL from which the binary\n// identified by the object name suffix can be downloaded using the\n// HTTP protocol. If not all parts of the binary have been uploaded,\n// the method returns an error.", + OutArgs: []rpc.ArgDesc{ + {"url", ``}, // string + {"ttl", ``}, // int64 + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))}, + }, + { + Name: "Stat", + Doc: "// Stat returns information describing the parts of the binary\n// identified by the object name suffix, and its RFC 2046 media type.\n// If the binary has not been created, the method returns an error.", + OutArgs: []rpc.ArgDesc{ + {"Parts", ``}, // []binary.PartInfo + {"MediaInfo", ``}, // MediaInfo + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))}, + }, + { + Name: "Upload", + Doc: "// Upload opens a stream that can be used for uploading the given\n// part of the binary identified by the object name suffix. If the\n// binary has not been created, the method returns an error. If the\n// binary part has been uploaded, the method returns an error. If\n// the same binary part is being uploaded by another caller, the\n// method returns an error.", + InArgs: []rpc.ArgDesc{ + {"part", ``}, // int32 + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))}, + }, + }, +} + +// BinaryDownloadServerStream is the server stream for Binary.Download. +type BinaryDownloadServerStream interface { + // SendStream returns the send side of the Binary.Download server stream. + SendStream() interface { + // Send places the item onto the output stream. Returns errors encountered + // while sending. Blocks if there is no buffer space; will unblock when + // buffer space is available. + Send(item []byte) error + } +} + +// BinaryDownloadServerCall represents the context passed to Binary.Download. +type BinaryDownloadServerCall interface { + rpc.ServerCall + BinaryDownloadServerStream +} + +// BinaryDownloadServerCallStub is a wrapper that converts rpc.StreamServerCall into +// a typesafe stub that implements BinaryDownloadServerCall. +type BinaryDownloadServerCallStub struct { + rpc.StreamServerCall +} + +// Init initializes BinaryDownloadServerCallStub from rpc.StreamServerCall. +func (s *BinaryDownloadServerCallStub) Init(call rpc.StreamServerCall) { + s.StreamServerCall = call +} + +// SendStream returns the send side of the Binary.Download server stream. +func (s *BinaryDownloadServerCallStub) SendStream() interface { + Send(item []byte) error +} { + return implBinaryDownloadServerCallSend{s} +} + +type implBinaryDownloadServerCallSend struct { + s *BinaryDownloadServerCallStub +} + +func (s implBinaryDownloadServerCallSend) Send(item []byte) error { + return s.s.Send(item) +} + +// BinaryUploadServerStream is the server stream for Binary.Upload. +type BinaryUploadServerStream interface { + // RecvStream returns the receiver side of the Binary.Upload server stream. + RecvStream() interface { + // Advance stages an item so that it may be retrieved via Value. Returns + // true iff there is an item to retrieve. Advance must be called before + // Value is called. May block if an item is not available. + Advance() bool + // Value returns the item that was staged by Advance. May panic if Advance + // returned false or was not called. Never blocks. + Value() []byte + // Err returns any error encountered by Advance. Never blocks. + Err() error + } +} + +// BinaryUploadServerCall represents the context passed to Binary.Upload. +type BinaryUploadServerCall interface { + rpc.ServerCall + BinaryUploadServerStream +} + +// BinaryUploadServerCallStub is a wrapper that converts rpc.StreamServerCall into +// a typesafe stub that implements BinaryUploadServerCall. +type BinaryUploadServerCallStub struct { + rpc.StreamServerCall + valRecv []byte + errRecv error +} + +// Init initializes BinaryUploadServerCallStub from rpc.StreamServerCall. +func (s *BinaryUploadServerCallStub) Init(call rpc.StreamServerCall) { + s.StreamServerCall = call +} + +// RecvStream returns the receiver side of the Binary.Upload server stream. +func (s *BinaryUploadServerCallStub) RecvStream() interface { + Advance() bool + Value() []byte + Err() error +} { + return implBinaryUploadServerCallRecv{s} +} + +type implBinaryUploadServerCallRecv struct { + s *BinaryUploadServerCallStub +} + +func (s implBinaryUploadServerCallRecv) Advance() bool { + s.s.errRecv = s.s.Recv(&s.s.valRecv) + return s.s.errRecv == nil +} +func (s implBinaryUploadServerCallRecv) Value() []byte { + return s.s.valRecv +} +func (s implBinaryUploadServerCallRecv) Err() error { + if s.s.errRecv == io.EOF { + return nil + } + return s.s.errRecv +} + +// ProfileClientMethods is the client interface +// containing Profile methods. +// +// Profile abstracts a device's ability to run binaries, and hides +// specifics such as the operating system, hardware architecture, and +// the set of installed libraries. Profiles describe binaries and +// devices, and are used to match them. +type ProfileClientMethods interface { + // Label is the human-readable profile key for the profile, + // e.g. "linux-media". The label can be used to uniquely identify + // the profile (for the purpose of matching application binaries and + // devices). + Label(*context.T, ...rpc.CallOpt) (string, error) + // Description is a free-text description of the profile, meant for + // human consumption. + Description(*context.T, ...rpc.CallOpt) (string, error) +} + +// ProfileClientStub adds universal methods to ProfileClientMethods. +type ProfileClientStub interface { + ProfileClientMethods + rpc.UniversalServiceMethods +} + +// ProfileClient returns a client stub for Profile. +func ProfileClient(name string) ProfileClientStub { + return implProfileClientStub{name} +} + +type implProfileClientStub struct { + name string +} + +func (c implProfileClientStub) Label(ctx *context.T, opts ...rpc.CallOpt) (o0 string, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Label", nil, []interface{}{&o0}, opts...) + return +} + +func (c implProfileClientStub) Description(ctx *context.T, opts ...rpc.CallOpt) (o0 string, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Description", nil, []interface{}{&o0}, opts...) + return +} + +// ProfileServerMethods is the interface a server writer +// implements for Profile. +// +// Profile abstracts a device's ability to run binaries, and hides +// specifics such as the operating system, hardware architecture, and +// the set of installed libraries. Profiles describe binaries and +// devices, and are used to match them. +type ProfileServerMethods interface { + // Label is the human-readable profile key for the profile, + // e.g. "linux-media". The label can be used to uniquely identify + // the profile (for the purpose of matching application binaries and + // devices). + Label(*context.T, rpc.ServerCall) (string, error) + // Description is a free-text description of the profile, meant for + // human consumption. + Description(*context.T, rpc.ServerCall) (string, error) +} + +// ProfileServerStubMethods is the server interface containing +// Profile methods, as expected by rpc.Server. +// There is no difference between this interface and ProfileServerMethods +// since there are no streaming methods. +type ProfileServerStubMethods ProfileServerMethods + +// ProfileServerStub adds universal methods to ProfileServerStubMethods. +type ProfileServerStub interface { + ProfileServerStubMethods + // Describe the Profile interfaces. + Describe__() []rpc.InterfaceDesc +} + +// ProfileServer returns a server stub for Profile. +// It converts an implementation of ProfileServerMethods into +// an object that may be used by rpc.Server. +func ProfileServer(impl ProfileServerMethods) ProfileServerStub { + stub := implProfileServerStub{ + impl: impl, + } + // Initialize GlobState; always check the stub itself first, to handle the + // case where the user has the Glob method defined in their VDL source. + if gs := rpc.NewGlobState(stub); gs != nil { + stub.gs = gs + } else if gs := rpc.NewGlobState(impl); gs != nil { + stub.gs = gs + } + return stub +} + +type implProfileServerStub struct { + impl ProfileServerMethods + gs *rpc.GlobState +} + +func (s implProfileServerStub) Label(ctx *context.T, call rpc.ServerCall) (string, error) { + return s.impl.Label(ctx, call) +} + +func (s implProfileServerStub) Description(ctx *context.T, call rpc.ServerCall) (string, error) { + return s.impl.Description(ctx, call) +} + +func (s implProfileServerStub) Globber() *rpc.GlobState { + return s.gs +} + +func (s implProfileServerStub) Describe__() []rpc.InterfaceDesc { + return []rpc.InterfaceDesc{ProfileDesc} +} + +// ProfileDesc describes the Profile interface. +var ProfileDesc rpc.InterfaceDesc = descProfile + +// descProfile hides the desc to keep godoc clean. +var descProfile = rpc.InterfaceDesc{ + Name: "Profile", + PkgPath: "v.io/v23/services/repository", + Doc: "// Profile abstracts a device's ability to run binaries, and hides\n// specifics such as the operating system, hardware architecture, and\n// the set of installed libraries. Profiles describe binaries and\n// devices, and are used to match them.", + Methods: []rpc.MethodDesc{ + { + Name: "Label", + Doc: "// Label is the human-readable profile key for the profile,\n// e.g. \"linux-media\". The label can be used to uniquely identify\n// the profile (for the purpose of matching application binaries and\n// devices).", + OutArgs: []rpc.ArgDesc{ + {"", ``}, // string + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))}, + }, + { + Name: "Description", + Doc: "// Description is a free-text description of the profile, meant for\n// human consumption.", + OutArgs: []rpc.ArgDesc{ + {"", ``}, // string + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))}, + }, + }, +} + +// Hold type definitions in package-level variables, for better performance. +var ( + __VDLType_struct_1 *vdl.Type +) + +var __VDLInitCalled bool + +// __VDLInit performs vdl initialization. It is safe to call multiple times. +// If you have an init ordering issue, just insert the following line verbatim +// into your source files in this package, right after the "package foo" clause: +// +// var _ = __VDLInit() +// +// The purpose of this function is to ensure that vdl initialization occurs in +// the right order, and very early in the init sequence. In particular, vdl +// registration and package variable initialization needs to occur before +// functions like vdl.TypeOf will work properly. +// +// This function returns a dummy value, so that it can be used to initialize the +// first var in the file, to take advantage of Go's defined init order. +func __VDLInit() struct{} { + if __VDLInitCalled { + return struct{}{} + } + __VDLInitCalled = true + + // Register types. + vdl.Register((*MediaInfo)(nil)) + + // Initialize type definitions. + __VDLType_struct_1 = vdl.TypeOf((*MediaInfo)(nil)).Elem() + + return struct{}{} +} diff --git a/v23/services/tidyable/service.vdl b/v23/services/tidyable/service.vdl new file mode 100644 index 000000000..49490550e --- /dev/null +++ b/v23/services/tidyable/service.vdl @@ -0,0 +1,18 @@ +// Copyright 2015 The Vanadium 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 tidyable defines an interface for services that can be +// requested to clean up transient resource use (such as logs or caches.) +package tidyable + +import ( + "v.io/v23/security/access" +) + +// Tidyable specifies that a service can be tidied. +type Tidyable interface { + // Request the implementing service to perform regularly scheduled cleanup + // actions such as shrinking caches or rolling logs immediately. + TidyNow() error {access.Admin} +} diff --git a/v23/services/tidyable/tidyable.vdl.go b/v23/services/tidyable/tidyable.vdl.go new file mode 100644 index 000000000..8ba05dc2c --- /dev/null +++ b/v23/services/tidyable/tidyable.vdl.go @@ -0,0 +1,151 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated by the vanadium vdl tool. +// Package: tidyable + +// Package tidyable defines an interface for services that can be +// requested to clean up transient resource use (such as logs or caches.) +package tidyable + +import ( + "v.io/v23" + "v.io/v23/context" + "v.io/v23/rpc" + "v.io/v23/security/access" + "v.io/v23/vdl" +) + +var _ = __VDLInit() // Must be first; see __VDLInit comments for details. + +////////////////////////////////////////////////// +// Interface definitions + +// TidyableClientMethods is the client interface +// containing Tidyable methods. +// +// Tidyable specifies that a service can be tidied. +type TidyableClientMethods interface { + // Request the implementing service to perform regularly scheduled cleanup + // actions such as shrinking caches or rolling logs immediately. + TidyNow(*context.T, ...rpc.CallOpt) error +} + +// TidyableClientStub adds universal methods to TidyableClientMethods. +type TidyableClientStub interface { + TidyableClientMethods + rpc.UniversalServiceMethods +} + +// TidyableClient returns a client stub for Tidyable. +func TidyableClient(name string) TidyableClientStub { + return implTidyableClientStub{name} +} + +type implTidyableClientStub struct { + name string +} + +func (c implTidyableClientStub) TidyNow(ctx *context.T, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "TidyNow", nil, nil, opts...) + return +} + +// TidyableServerMethods is the interface a server writer +// implements for Tidyable. +// +// Tidyable specifies that a service can be tidied. +type TidyableServerMethods interface { + // Request the implementing service to perform regularly scheduled cleanup + // actions such as shrinking caches or rolling logs immediately. + TidyNow(*context.T, rpc.ServerCall) error +} + +// TidyableServerStubMethods is the server interface containing +// Tidyable methods, as expected by rpc.Server. +// There is no difference between this interface and TidyableServerMethods +// since there are no streaming methods. +type TidyableServerStubMethods TidyableServerMethods + +// TidyableServerStub adds universal methods to TidyableServerStubMethods. +type TidyableServerStub interface { + TidyableServerStubMethods + // Describe the Tidyable interfaces. + Describe__() []rpc.InterfaceDesc +} + +// TidyableServer returns a server stub for Tidyable. +// It converts an implementation of TidyableServerMethods into +// an object that may be used by rpc.Server. +func TidyableServer(impl TidyableServerMethods) TidyableServerStub { + stub := implTidyableServerStub{ + impl: impl, + } + // Initialize GlobState; always check the stub itself first, to handle the + // case where the user has the Glob method defined in their VDL source. + if gs := rpc.NewGlobState(stub); gs != nil { + stub.gs = gs + } else if gs := rpc.NewGlobState(impl); gs != nil { + stub.gs = gs + } + return stub +} + +type implTidyableServerStub struct { + impl TidyableServerMethods + gs *rpc.GlobState +} + +func (s implTidyableServerStub) TidyNow(ctx *context.T, call rpc.ServerCall) error { + return s.impl.TidyNow(ctx, call) +} + +func (s implTidyableServerStub) Globber() *rpc.GlobState { + return s.gs +} + +func (s implTidyableServerStub) Describe__() []rpc.InterfaceDesc { + return []rpc.InterfaceDesc{TidyableDesc} +} + +// TidyableDesc describes the Tidyable interface. +var TidyableDesc rpc.InterfaceDesc = descTidyable + +// descTidyable hides the desc to keep godoc clean. +var descTidyable = rpc.InterfaceDesc{ + Name: "Tidyable", + PkgPath: "v.io/v23/services/tidyable", + Doc: "// Tidyable specifies that a service can be tidied.", + Methods: []rpc.MethodDesc{ + { + Name: "TidyNow", + Doc: "// Request the implementing service to perform regularly scheduled cleanup\n// actions such as shrinking caches or rolling logs immediately.", + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Admin"))}, + }, + }, +} + +var __VDLInitCalled bool + +// __VDLInit performs vdl initialization. It is safe to call multiple times. +// If you have an init ordering issue, just insert the following line verbatim +// into your source files in this package, right after the "package foo" clause: +// +// var _ = __VDLInit() +// +// The purpose of this function is to ensure that vdl initialization occurs in +// the right order, and very early in the init sequence. In particular, vdl +// registration and package variable initialization needs to occur before +// functions like vdl.TypeOf will work properly. +// +// This function returns a dummy value, so that it can be used to initialize the +// first var in the file, to take advantage of Go's defined init order. +func __VDLInit() struct{} { + if __VDLInitCalled { + return struct{}{} + } + __VDLInitCalled = true + + return struct{}{} +} diff --git a/x/ref/services/device/claimable/claimable_v23_test.go b/x/ref/services/device/claimable/claimable_v23_test.go new file mode 100644 index 000000000..579aeb5ac --- /dev/null +++ b/x/ref/services/device/claimable/claimable_v23_test.go @@ -0,0 +1,88 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + libsec "v.io/x/ref/lib/security" + "v.io/x/ref/test/v23test" +) + +func TestV23ClaimableServer(t *testing.T) { + v23test.SkipUnlessRunningIntegrationTests(t) + sh := v23test.NewShell(t, nil) + defer sh.Cleanup() + + workdir, err := ioutil.TempDir("", "claimable-test-") + if err != nil { + t.Fatalf("ioutil.TempDir failed: %v", err) + } + defer os.RemoveAll(workdir) + + permsDir := filepath.Join(workdir, "perms") + + serverCreds := sh.ForkCredentials("child") + if err := libsec.InitDefaultBlessings(serverCreds.Principal, "server"); err != nil { + t.Fatalf("Failed to create server credentials: %v", err) + } + legitClientCreds := sh.ForkCredentials("legit") + badClientCreds1 := sh.ForkCredentials("child") + badClientCreds2 := sh.ForkCredentials("other-guy") + + serverBin := v23test.BuildGoPkg(sh, "v.io/x/ref/services/device/claimable") + server := sh.Cmd(serverBin, + "--v23.tcp.address=127.0.0.1:0", + "--perms-dir="+permsDir, + "--root-blessings="+rootBlessings(t, sh, legitClientCreds), + "--v23.permissions.literal={\"Admin\":{\"In\":[\"root:legit\"]}}", + ).WithCredentials(serverCreds) + server.Start() + addr := server.S.ExpectVar("NAME") + + clientBin := v23test.BuildGoPkg(sh, "v.io/x/ref/services/device/device") + + testcases := []struct { + creds *v23test.Credentials + success bool + permsExist bool + }{ + {badClientCreds1, false, false}, + {badClientCreds2, false, false}, + {legitClientCreds, true, true}, + } + + for _, tc := range testcases { + client := sh.Cmd(clientBin, "claim", addr, "my-device").WithCredentials(tc.creds) + client.ExitErrorIsOk = true + if client.Run(); (client.Err == nil) != tc.success { + t.Errorf("Unexpected exit value. Expected success=%v, got err=%v", tc.success, err) + } + if _, err := os.Stat(permsDir); (client.Err == nil) != tc.permsExist { + t.Errorf("Unexpected permsDir state. Got %v, expected %v", err == nil, tc.permsExist) + } + } + + // Server should exit cleanly after the successful Claim. + server.Wait() +} + +// Note: Identical to rootBlessings in +// v.io/x/ref/services/cluster/cluster_agentd/cluster_agentd_v23_test.go. +func rootBlessings(t *testing.T, sh *v23test.Shell, creds *v23test.Credentials) string { + principalBin := v23test.BuildGoPkg(sh, "v.io/x/ref/cmd/principal") + blessings := strings.TrimSpace(sh.Cmd(principalBin, "get", "default").WithCredentials(creds).Stdout()) + cmd := sh.Cmd(principalBin, "dumproots", "-") + cmd.SetStdinReader(strings.NewReader(blessings)) + return strings.Replace(strings.TrimSpace(cmd.Stdout()), "\n", ",", -1) +} + +func TestMain(m *testing.M) { + v23test.TestMain(m) +} diff --git a/x/ref/services/device/claimable/doc.go b/x/ref/services/device/claimable/doc.go new file mode 100644 index 000000000..eb7b62539 --- /dev/null +++ b/x/ref/services/device/claimable/doc.go @@ -0,0 +1,82 @@ +// Copyright 2018 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated via go generate. +// DO NOT UPDATE MANUALLY + +/* +Claimable is a server that implements the Claimable interface from +v.io/v23/services/device. It exits immediately if the device is already claimed. +Otherwise, it keeps running until a successful Claim() request is received. + +It uses -v23.permissions.* to authorize the Claim request. + +Usage: + claimable [flags] + +The claimable flags are: + -perms-dir= + The directory where permissions will be stored. + -root-blessings= + A comma-separated list of the root blessings to trust, base64-encoded + VOM-encoded. + +The global flags are: + -alsologtostderr=true + log to standard error as well as files + -log_backtrace_at=:0 + when logging hits line file:N, emit a stack trace + -log_dir= + if non-empty, write log files to this directory + -logtostderr=false + log to standard error instead of files + -max_stack_buf_size=4292608 + max size in bytes of the buffer to use for logging stack traces + -metadata= + Displays metadata for the program and exits. + -stderrthreshold=2 + logs at or above this threshold go to stderr + -time=false + Dump timing information to stderr before exiting the program. + -v=0 + log level for V logs + -v23.credentials= + directory to use for storing security credentials + -v23.i18n-catalogue= + 18n catalogue files to load, comma separated + -v23.namespace.root=[/(dev.v.io:r:vprod:service:mounttabled)@ns.dev.v.io:8101] + local namespace root; can be repeated to provided multiple roots + -v23.permissions.file= + specify a perms file as : + -v23.permissions.literal= + explicitly specify the runtime perms as a JSON-encoded access.Permissions. + Overrides all --v23.permissions.file flags + -v23.proxy= + object name of proxy service to use to export services across network + boundaries + -v23.tcp.address= + address to listen on + -v23.tcp.protocol= + protocol to listen with + -v23.vtrace.cache-size=1024 + The number of vtrace traces to store in memory + -v23.vtrace.collect-regexp= + Spans and annotations that match this regular expression will trigger trace + collection + -v23.vtrace.dump-on-shutdown=true + If true, dump all stored traces on runtime shutdown + -v23.vtrace.sample-rate=0 + Rate (from 0.0 to 1.0) to sample vtrace traces + -v23.vtrace.v=0 + The verbosity level of the log messages to be captured in traces + -vmodule= + comma-separated list of globpattern=N settings for filename-filtered logging + (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns baz or + *az or b* but not by bar/baz or baz.go or az or b.* + -vpath= + comma-separated list of regexppattern=N settings for file pathname-filtered + logging (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns + foo/bar/baz or fo.*az or oo/ba or b.z but not by foo/bar/baz.go or fo*az +*/ +package main diff --git a/x/ref/services/device/claimable/main.go b/x/ref/services/device/claimable/main.go new file mode 100644 index 000000000..2a2912561 --- /dev/null +++ b/x/ref/services/device/claimable/main.go @@ -0,0 +1,98 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The following enables go generate to generate the doc.go file. +//go:generate gendoc . -help + +package main + +import ( + "encoding/base64" + "errors" + "fmt" + "strings" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/security" + "v.io/v23/vom" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/security/securityflag" + "v.io/x/ref/lib/signals" + "v.io/x/ref/lib/v23cmd" + _ "v.io/x/ref/runtime/factories/roaming" + "v.io/x/ref/services/device/internal/claim" +) + +var ( + permsDir string + rootBlessings string +) + +func runServer(ctx *context.T, _ *cmdline.Env, _ []string) error { + if rootBlessings != "" { + addRoot(ctx, rootBlessings) + } + + auth := securityflag.NewAuthorizerOrDie(ctx) + claimable, claimed := claim.NewClaimableDispatcher(ctx, permsDir, "", auth) + if claimable == nil { + return errors.New("device is already claimed") + } + + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", claimable) + if err != nil { + return err + } + + status := server.Status() + ctx.Infof("Listening on: %v", status.Endpoints) + if len(status.Endpoints) > 0 { + fmt.Printf("NAME=%s\n", status.Endpoints[0].Name()) + } + select { + case <-claimed: + return nil + case s := <-signals.ShutdownOnSignals(ctx): + return fmt.Errorf("received signal %v", s) + } +} + +func addRoot(ctx *context.T, flagRoots string) { + p := v23.GetPrincipal(ctx) + for _, b64 := range strings.Split(flagRoots, ",") { + // We use URLEncoding to be compatible with the principal + // command. + vomBlessings, err := base64.URLEncoding.DecodeString(b64) + if err != nil { + ctx.Fatalf("unable to decode the base64 blessing roots: %v", err) + } + var blessings security.Blessings + if err := vom.Decode(vomBlessings, &blessings); err != nil { + ctx.Fatalf("unable to decode the vom blessing roots: %v", err) + } + if err := security.AddToRoots(p, blessings); err != nil { + ctx.Fatalf("unable to add blessing roots: %v", err) + } + } +} + +func main() { + rootCmd := &cmdline.Command{ + Name: "claimable", + Short: "Run claimable server", + Long: ` +Claimable is a server that implements the Claimable interface from +v.io/v23/services/device. It exits immediately if the device is already +claimed. Otherwise, it keeps running until a successful Claim() request +is received. + +It uses -v23.permissions.* to authorize the Claim request. +`, + Runner: v23cmd.RunnerFunc(runServer), + } + rootCmd.Flags.StringVar(&permsDir, "perms-dir", "", "The directory where permissions will be stored.") + rootCmd.Flags.StringVar(&rootBlessings, "root-blessings", "", "A comma-separated list of the root blessings to trust, base64-encoded VOM-encoded.") + cmdline.Main(rootCmd) +} diff --git a/x/ref/services/device/config.vdl b/x/ref/services/device/config.vdl new file mode 100644 index 000000000..459fccf91 --- /dev/null +++ b/x/ref/services/device/config.vdl @@ -0,0 +1,11 @@ +// Copyright 2015 The Vanadium 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 device + +// Config is an RPC API to the config service. +type Config interface { + // Set sets the value for key. + Set(key, value string) error +} diff --git a/x/ref/services/device/device.vdl.go b/x/ref/services/device/device.vdl.go new file mode 100644 index 000000000..e0f22b5f9 --- /dev/null +++ b/x/ref/services/device/device.vdl.go @@ -0,0 +1,148 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated by the vanadium vdl tool. +// Package: device + +package device + +import ( + "v.io/v23" + "v.io/v23/context" + "v.io/v23/rpc" +) + +var _ = __VDLInit() // Must be first; see __VDLInit comments for details. + +////////////////////////////////////////////////// +// Interface definitions + +// ConfigClientMethods is the client interface +// containing Config methods. +// +// Config is an RPC API to the config service. +type ConfigClientMethods interface { + // Set sets the value for key. + Set(_ *context.T, key string, value string, _ ...rpc.CallOpt) error +} + +// ConfigClientStub adds universal methods to ConfigClientMethods. +type ConfigClientStub interface { + ConfigClientMethods + rpc.UniversalServiceMethods +} + +// ConfigClient returns a client stub for Config. +func ConfigClient(name string) ConfigClientStub { + return implConfigClientStub{name} +} + +type implConfigClientStub struct { + name string +} + +func (c implConfigClientStub) Set(ctx *context.T, i0 string, i1 string, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Set", []interface{}{i0, i1}, nil, opts...) + return +} + +// ConfigServerMethods is the interface a server writer +// implements for Config. +// +// Config is an RPC API to the config service. +type ConfigServerMethods interface { + // Set sets the value for key. + Set(_ *context.T, _ rpc.ServerCall, key string, value string) error +} + +// ConfigServerStubMethods is the server interface containing +// Config methods, as expected by rpc.Server. +// There is no difference between this interface and ConfigServerMethods +// since there are no streaming methods. +type ConfigServerStubMethods ConfigServerMethods + +// ConfigServerStub adds universal methods to ConfigServerStubMethods. +type ConfigServerStub interface { + ConfigServerStubMethods + // Describe the Config interfaces. + Describe__() []rpc.InterfaceDesc +} + +// ConfigServer returns a server stub for Config. +// It converts an implementation of ConfigServerMethods into +// an object that may be used by rpc.Server. +func ConfigServer(impl ConfigServerMethods) ConfigServerStub { + stub := implConfigServerStub{ + impl: impl, + } + // Initialize GlobState; always check the stub itself first, to handle the + // case where the user has the Glob method defined in their VDL source. + if gs := rpc.NewGlobState(stub); gs != nil { + stub.gs = gs + } else if gs := rpc.NewGlobState(impl); gs != nil { + stub.gs = gs + } + return stub +} + +type implConfigServerStub struct { + impl ConfigServerMethods + gs *rpc.GlobState +} + +func (s implConfigServerStub) Set(ctx *context.T, call rpc.ServerCall, i0 string, i1 string) error { + return s.impl.Set(ctx, call, i0, i1) +} + +func (s implConfigServerStub) Globber() *rpc.GlobState { + return s.gs +} + +func (s implConfigServerStub) Describe__() []rpc.InterfaceDesc { + return []rpc.InterfaceDesc{ConfigDesc} +} + +// ConfigDesc describes the Config interface. +var ConfigDesc rpc.InterfaceDesc = descConfig + +// descConfig hides the desc to keep godoc clean. +var descConfig = rpc.InterfaceDesc{ + Name: "Config", + PkgPath: "v.io/x/ref/services/device", + Doc: "// Config is an RPC API to the config service.", + Methods: []rpc.MethodDesc{ + { + Name: "Set", + Doc: "// Set sets the value for key.", + InArgs: []rpc.ArgDesc{ + {"key", ``}, // string + {"value", ``}, // string + }, + }, + }, +} + +var __VDLInitCalled bool + +// __VDLInit performs vdl initialization. It is safe to call multiple times. +// If you have an init ordering issue, just insert the following line verbatim +// into your source files in this package, right after the "package foo" clause: +// +// var _ = __VDLInit() +// +// The purpose of this function is to ensure that vdl initialization occurs in +// the right order, and very early in the init sequence. In particular, vdl +// registration and package variable initialization needs to occur before +// functions like vdl.TypeOf will work properly. +// +// This function returns a dummy value, so that it can be used to initialize the +// first var in the file, to take advantage of Go's defined init order. +func __VDLInit() struct{} { + if __VDLInitCalled { + return struct{}{} + } + __VDLInitCalled = true + + return struct{}{} +} diff --git a/x/ref/services/device/device/acl.go b/x/ref/services/device/device/acl.go new file mode 100644 index 000000000..3c9973ef9 --- /dev/null +++ b/x/ref/services/device/device/acl.go @@ -0,0 +1,146 @@ +// Copyright 2015 The Vanadium 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 main + +// Commands to get/set Permissions. + +import ( + "fmt" + + "v.io/v23/context" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/device" + "v.io/v23/verror" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" +) + +var cmdGet = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runGet), + Name: "get", + Short: "Get Permissions for the given target.", + Long: "Get Permissions for the given target.", + ArgsName: "", + ArgsLong: ` + can be a Vanadium name for a device manager, +application installation or instance.`, +} + +func runGet(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 1, len(args); expected != got { + return env.UsageErrorf("get: incorrect number of arguments, expected %d, got %d", expected, got) + } + + vanaName := args[0] + objPerms, _, err := device.ApplicationClient(vanaName).GetPermissions(ctx) + if err != nil { + return fmt.Errorf("GetPermissions on %s failed: %v", vanaName, err) + } + // Convert objPerms (Permissions) into permsEntries for pretty printing. + entries := make(permsEntries) + for tag, perms := range objPerms { + for _, p := range perms.In { + entries.Tags(string(p))[tag] = false + } + for _, b := range perms.NotIn { + entries.Tags(b)[tag] = true + } + } + fmt.Fprintln(env.Stdout, entries) + return nil +} + +// TODO(caprita): Add unit test logic for 'force set'. +var forceSet bool + +var cmdSet = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runSet), + Name: "set", + Short: "Set Permissions for the given target.", + Long: "Set Permissions for the given target", + ArgsName: " ( [!](,[!])*", + ArgsLong: ` + can be a Vanadium name for a device manager, +application installation or instance. + + is a blessing pattern. +If the same pattern is repeated multiple times in the command, then +the only the last occurrence will be honored. + + is a subset of defined access types ("Admin", "Read", "Write" etc.). +If the access right is prefixed with a '!' then is added to the +NotIn list for that right. Using "^" as a "tag" causes all occurrences of + in the current AccessList to be cleared. + +Examples: +set root/self ^ +will remove "root/self" from the In and NotIn lists for all access rights. + +set root/self Read,!Write +will add "root/self" to the In list for Read access and the NotIn list +for Write access (and remove "root/self" from both the In and NotIn +lists of all other access rights)`, +} + +func init() { + cmdSet.Flags.BoolVar(&forceSet, "f", false, "Instead of making the AccessLists additive, do a complete replacement based on the specified settings.") +} + +func runSet(ctx *context.T, env *cmdline.Env, args []string) error { + if got := len(args); !((got%2) == 1 && got >= 3) { + return env.UsageErrorf("set: incorrect number of arguments %d, must be 1 + 2n", got) + } + + vanaName := args[0] + pairs := args[1:] + + entries := make(permsEntries) + for i := 0; i < len(pairs); i += 2 { + blessing := pairs[i] + tags, err := parseAccessTags(pairs[i+1]) + if err != nil { + return env.UsageErrorf("failed to parse access tags for %q: %v", blessing, err) + } + entries[blessing] = tags + } + + // Set the Permissions on the specified names. + for { + objPerms, version := make(access.Permissions), "" + if !forceSet { + var err error + if objPerms, version, err = device.ApplicationClient(vanaName).GetPermissions(ctx); err != nil { + return fmt.Errorf("GetPermissions(%s) failed: %v", vanaName, err) + } + } + for blessingOrPattern, tags := range entries { + objPerms.Clear(blessingOrPattern) // Clear out any existing references + for tag, blacklist := range tags { + if blacklist { + objPerms.Blacklist(blessingOrPattern, tag) + } else { + objPerms.Add(security.BlessingPattern(blessingOrPattern), tag) + } + } + } + switch err := device.ApplicationClient(vanaName).SetPermissions(ctx, objPerms, version); { + case err != nil && verror.ErrorID(err) != verror.ErrBadVersion.ID: + return fmt.Errorf("SetPermissions(%s) failed: %v", vanaName, err) + case err == nil: + return nil + } + fmt.Fprintln(env.Stderr, "WARNING: trying again because of asynchronous change") + } +} + +var cmdACL = &cmdline.Command{ + Name: "acl", + Short: "Tool for setting device manager Permissions", + Long: ` +The acl tool manages Permissions on the device manger, installations and instances. +`, + Children: []*cmdline.Command{cmdGet, cmdSet}, +} diff --git a/x/ref/services/device/device/acl_fmt.go b/x/ref/services/device/device/acl_fmt.go new file mode 100644 index 000000000..4089fa716 --- /dev/null +++ b/x/ref/services/device/device/acl_fmt.go @@ -0,0 +1,86 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + "sort" + "strings" + + "v.io/v23/security" + "v.io/x/lib/set" +) + +// permsEntries maps blessing patterns to the kind of access they should have. +type permsEntries map[string]accessTags + +// accessTags maps access tags to whether they should be blacklisted +// (i.e., part of the NotIn list) or not (part of the In list). +// +// TODO(ashankar,caprita): This structure is not friendly to a blessing +// appearing in both the "In" and "NotIn" lists of an AccessList. Arguably, such +// an AccessList is silly (In: ["foo"], NotIn: ["foo"]), but is legal. This +// structure can end up hiding that. +type accessTags map[string]bool + +// String representation of access tags. Between String and parseAccessTags, +// the "get" and "set" commands are able to speak the same language: the output +// of "get" and be copied/pasted into "set". +func (tags accessTags) String() string { + // Sort tags and then apply "!". + list := set.StringBool.ToSlice(tags) + sort.Strings(list) + for ix, tag := range list { + if tags[tag] { + list[ix] = "!" + list[ix] + } + } + return strings.Join(list, ",") +} + +func parseAccessTags(input string) (accessTags, error) { + ret := make(accessTags) + if input == "^" { + return ret, nil + } + for _, tag := range strings.Split(input, ",") { + blacklist := strings.HasPrefix(tag, "!") + if blacklist { + tag = tag[1:] + } + if len(tag) == 0 { + return nil, fmt.Errorf("empty access tag in %q", input) + } + ret[tag] = blacklist + } + return ret, nil +} + +func (entries permsEntries) String() string { + var list []string + for pattern, _ := range entries { + list = append(list, pattern) + } + sort.Strings(list) + for ix, pattern := range list { + list[ix] = fmt.Sprintf("%s %v", pattern, entries[pattern]) + } + return strings.Join(list, "\n") +} + +func (entries permsEntries) Tags(pattern string) accessTags { + tags, exists := entries[pattern] + if !exists { + tags = make(accessTags) + entries[pattern] = tags + } + return tags +} + +type byPattern []security.BlessingPattern + +func (a byPattern) Len() int { return len(a) } +func (a byPattern) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byPattern) Less(i, j int) bool { return a[i] < a[j] } diff --git a/x/ref/services/device/device/acl_test.go b/x/ref/services/device/device/acl_test.go new file mode 100644 index 000000000..6b19ba251 --- /dev/null +++ b/x/ref/services/device/device/acl_test.go @@ -0,0 +1,302 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "reflect" + "regexp" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/verror" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/test" + + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" +) + +const pkgPath = "v.io/x/ref/services/device/main" + +var ( + errOops = verror.Register(pkgPath+".errOops", verror.NoRetry, "oops!") +) + +func TestAccessListGetCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + + // Setup the command-line. + cmd := cmd_device.CmdRoot + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + deviceName := server.Status().Endpoints[0].Name() + + // Test the 'get' command. + rootTape := tapes.ForSuffix("") + rootTape.SetResponses(GetPermissionsResponse{ + perms: access.Permissions{ + "Admin": access.AccessList{ + In: []security.BlessingPattern{"self"}, + NotIn: []string{"self/bad"}, + }, + "Read": access.AccessList{ + In: []security.BlessingPattern{"other", "self"}, + }, + }, + version: "aVersionForToday", + err: nil, + }) + + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"acl", "get", deviceName}); err != nil { + t.Fatalf("error: %v", err) + } + if expected, got := strings.TrimSpace(` +other Read +self Admin,Read +self/bad !Admin +`), strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected output from get. Got %q, expected %q", got, expected) + } + if got, expected := rootTape.Play(), []interface{}{"GetPermissions"}; !reflect.DeepEqual(expected, got) { + t.Errorf("invalid call sequence. Got %#v, want %#v", got, expected) + } +} + +func TestAccessListSetCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + + // Setup the command-line. + cmd := cmd_device.CmdRoot + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + deviceName := server.Status().Endpoints[0].Name() + + // Some tests to validate parse. + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"acl", "set", deviceName}); err == nil { + t.Fatalf("failed to correctly detect insufficient parameters") + } + if expected, got := "ERROR: set: incorrect number of arguments 1, must be 1 + 2n", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected) + } + + stderr.Reset() + stdout.Reset() + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"acl", "set", deviceName, "foo"}); err == nil { + t.Fatalf("failed to correctly detect insufficient parameters") + } + if expected, got := "ERROR: set: incorrect number of arguments 2, must be 1 + 2n", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected) + } + + stderr.Reset() + stdout.Reset() + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"acl", "set", deviceName, "foo", "bar", "ohno"}); err == nil { + t.Fatalf("failed to correctly detect insufficient parameters") + } + if expected, got := "ERROR: set: incorrect number of arguments 4, must be 1 + 2n", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected) + } + + stderr.Reset() + stdout.Reset() + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"acl", "set", deviceName, "foo", "!"}); err == nil { + t.Fatalf("failed to detect invalid parameter") + } + if expected, got := "ERROR: failed to parse access tags for \"foo\": empty access tag", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Errorf("Unexpected output from list. Got %q, expected prefix %q", got, expected) + } + + // Correct operation in the absence of errors. + stderr.Reset() + stdout.Reset() + rootTape := tapes.ForSuffix("") + rootTape.SetResponses( + GetPermissionsResponse{ + perms: access.Permissions{ + "Admin": access.AccessList{ + In: []security.BlessingPattern{"self"}, + }, + "Read": access.AccessList{ + In: []security.BlessingPattern{"other", "self"}, + NotIn: []string{"other/bob"}, + }, + }, + version: "aVersionForToday", + err: nil, + }, + verror.NewErrBadVersion(nil), + GetPermissionsResponse{ + perms: access.Permissions{ + "Admin": access.AccessList{ + In: []security.BlessingPattern{"self"}, + }, + "Read": access.AccessList{ + In: []security.BlessingPattern{"other", "self"}, + NotIn: []string{"other/bob/baddevice"}, + }, + }, + version: "aVersionForTomorrow", + err: nil, + }, + nil, + ) + + // set command that: + // - Adds entry for "friends" to "Write" & "Admin" + // - Adds a blacklist entry for "friend/alice" for "Admin" + // - Edits existing entry for "self" (adding "Write" access) + // - Removes entry for "other/bob/baddevice" + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{ + "acl", + "set", + deviceName, + "friends", "Admin,Write", + "friends/alice", "!Admin,Write", + "self", "Admin,Write,Read", + "other/bob/baddevice", "^", + }); err != nil { + t.Fatalf("SetPermissions failed: %v", err) + } + + if expected, got := "", strings.TrimSpace(stdout.String()); got != expected { + t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected) + } + if expected, got := "WARNING: trying again because of asynchronous change", strings.TrimSpace(stderr.String()); got != expected { + t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected) + } + expected := []interface{}{ + "GetPermissions", + SetPermissionsStimulus{ + fun: "SetPermissions", + perms: access.Permissions{ + "Admin": access.AccessList{ + In: []security.BlessingPattern{"friends", "self"}, + NotIn: []string{"friends/alice"}, + }, + "Read": access.AccessList{ + In: []security.BlessingPattern{"other", "self"}, + NotIn: []string{"other/bob"}, + }, + "Write": access.AccessList{ + In: []security.BlessingPattern{"friends", "friends/alice", "self"}, + NotIn: []string(nil), + }, + }, + version: "aVersionForToday", + }, + "GetPermissions", + SetPermissionsStimulus{ + fun: "SetPermissions", + perms: access.Permissions{ + "Admin": access.AccessList{ + In: []security.BlessingPattern{"friends", "self"}, + NotIn: []string{"friends/alice"}, + }, + "Read": access.AccessList{ + In: []security.BlessingPattern{"other", "self"}, + NotIn: []string(nil), + }, + "Write": access.AccessList{ + In: []security.BlessingPattern{"friends", "friends/alice", "self"}, + NotIn: []string(nil), + }, + }, + version: "aVersionForTomorrow", + }, + } + + if got := rootTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("invalid call sequence. Got %#v, want %#v", got, expected) + } + rootTape.Rewind() + stdout.Reset() + stderr.Reset() + + // GetPermissions fails. + rootTape.SetResponses(GetPermissionsResponse{ + perms: access.Permissions{}, + version: "aVersionForToday", + err: verror.New(errOops, nil), + }) + + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"acl", "set", deviceName, "vana/bad", "Read"}); err == nil { + t.Fatalf("GetPermissions RPC inside perms set command failed but error wrongly not detected") + } else if expected, got := `^GetPermissions\(`+deviceName+`\) failed:.*oops!`, err.Error(); !regexp.MustCompile(expected).MatchString(got) { + t.Fatalf("Unexpected output from list. Got %q, regexp %q", got, expected) + } + if expected, got := "", strings.TrimSpace(stdout.String()); got != expected { + t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected) + } + expected = []interface{}{ + "GetPermissions", + } + + if got := rootTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("invalid call sequence. Got %#v, want %#v", got, expected) + } + rootTape.Rewind() + stdout.Reset() + stderr.Reset() + + // SetPermissions fails with something other than a bad version failure. + rootTape.SetResponses( + GetPermissionsResponse{ + perms: access.Permissions{ + "Read": access.AccessList{ + In: []security.BlessingPattern{"other", "self"}, + }, + }, + version: "aVersionForToday", + err: nil, + }, + verror.New(errOops, nil), + ) + + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"acl", "set", deviceName, "friend", "Read"}); err == nil { + t.Fatalf("SetPermissions should have failed: %v", err) + } else if expected, got := `^SetPermissions\(`+deviceName+`\) failed:.*oops!`, err.Error(); !regexp.MustCompile(expected).MatchString(got) { + t.Fatalf("Unexpected output from list. Got %q, regexp %q", got, expected) + } + if expected, got := "", strings.TrimSpace(stdout.String()); got != expected { + t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected) + } + + expected = []interface{}{ + "GetPermissions", + SetPermissionsStimulus{ + fun: "SetPermissions", + perms: access.Permissions{ + "Read": access.AccessList{ + In: []security.BlessingPattern{"friend", "other", "self"}, + NotIn: []string(nil), + }, + }, + version: "aVersionForToday", + }, + } + + if got := rootTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("invalid call sequence. Got %#v, want %#v", got, expected) + } +} diff --git a/x/ref/services/device/device/associate.go b/x/ref/services/device/device/associate.go new file mode 100644 index 000000000..a22d3947e --- /dev/null +++ b/x/ref/services/device/device/associate.go @@ -0,0 +1,93 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + "time" + + "v.io/v23/context" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" +) + +var cmdList = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runList), + Name: "list", + Short: "Lists the account associations.", + Long: "Lists all account associations.", + ArgsName: ".", + ArgsLong: ` + is the name of the device manager to connect to.`, +} + +func runList(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 1, len(args); expected != got { + return env.UsageErrorf("list: incorrect number of arguments, expected %d, got %d", expected, got) + } + + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + assocs, err := device.DeviceClient(args[0]).ListAssociations(ctx) + if err != nil { + return fmt.Errorf("ListAssociations failed: %v", err) + } + + for _, a := range assocs { + fmt.Fprintf(env.Stdout, "%s %s\n", a.IdentityName, a.AccountName) + } + return nil +} + +var cmdAdd = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runAdd), + Name: "add", + Short: "Add the listed blessings with the specified system account.", + Long: "Add the listed blessings with the specified system account.", + ArgsName: " ...", + ArgsLong: ` + is the name of the device manager to connect to. + is the name of an account holder on the local system. +.. are the blessings to associate systemAccount with.`, +} + +func runAdd(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 3, len(args); got < expected { + return env.UsageErrorf("add: incorrect number of arguments, expected at least %d, got %d", expected, got) + } + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + return device.DeviceClient(args[0]).AssociateAccount(ctx, args[2:], args[1]) +} + +var cmdRemove = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runRemove), + Name: "remove", + Short: "Removes system accounts associated with the listed blessings.", + Long: "Removes system accounts associated with the listed blessings.", + ArgsName: " ...", + ArgsLong: ` + is the name of the device manager to connect to. +... is a list of blessings.`, +} + +func runRemove(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 2, len(args); got < expected { + return env.UsageErrorf("remove: incorrect number of arguments, expected at least %d, got %d", expected, got) + } + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + return device.DeviceClient(args[0]).AssociateAccount(ctx, args[1:], "") +} + +var cmdAssociate = &cmdline.Command{ + Name: "associate", + Short: "Tool for creating associations between Vanadium blessings and a system account", + Long: ` +The associate tool facilitates managing blessing to system account associations. +`, + Children: []*cmdline.Command{cmdList, cmdAdd, cmdRemove}, +} diff --git a/x/ref/services/device/device/associate_test.go b/x/ref/services/device/device/associate_test.go new file mode 100644 index 000000000..25b0003bd --- /dev/null +++ b/x/ref/services/device/device/associate_test.go @@ -0,0 +1,163 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "reflect" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/test" + + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" +) + +func TestListCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + + // Setup the command-line. + cmd := cmd_device.CmdRoot + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + deviceName := server.Status().Endpoints[0].Name() + + rootTape := tapes.ForSuffix("") + // Test the 'list' command. + rootTape.SetResponses(ListAssociationResponse{ + na: []device.Association{ + { + "root/self", + "alice_self_account", + }, + { + "root/other", + "alice_other_account", + }, + }, + err: nil, + }) + + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"associate", "list", deviceName}); err != nil { + t.Fatalf("%v", err) + } + if expected, got := "root/self alice_self_account\nroot/other alice_other_account", strings.TrimSpace(stdout.String()); got != expected { + t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected) + } + if got, expected := rootTape.Play(), []interface{}{"ListAssociations"}; !reflect.DeepEqual(expected, got) { + t.Errorf("invalid call sequence. Got %v, want %v", got, expected) + } + rootTape.Rewind() + stdout.Reset() + + // Test list with bad parameters. + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"associate", "list", deviceName, "hello"}); err == nil { + t.Fatalf("wrongly failed to receive a non-nil error.") + } + if got, expected := len(rootTape.Play()), 0; got != expected { + t.Errorf("invalid call sequence. Got %v, want %v", got, expected) + } +} + +func TestAddCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + + // Setup the command-line. + cmd := cmd_device.CmdRoot + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + deviceName := server.Status().Endpoints[0].Name() + + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"add", "one"}); err == nil { + t.Fatalf("wrongly failed to receive a non-nil error.") + } + rootTape := tapes.ForSuffix("") + if got, expected := len(rootTape.Play()), 0; got != expected { + t.Errorf("invalid call sequence. Got %v, want %v", got, expected) + } + rootTape.Rewind() + stdout.Reset() + + rootTape.SetResponses(nil) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"associate", "add", deviceName, "alice", "root/self"}); err != nil { + t.Fatalf("%v", err) + } + expected := []interface{}{ + AddAssociationStimulus{"AssociateAccount", []string{"root/self"}, "alice"}, + } + if got := rootTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("unexpected result. Got %v want %v", got, expected) + } + rootTape.Rewind() + stdout.Reset() + + rootTape.SetResponses(nil) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"associate", "add", deviceName, "alice", "root/other", "root/self"}); err != nil { + t.Fatalf("%v", err) + } + expected = []interface{}{ + AddAssociationStimulus{"AssociateAccount", []string{"root/other", "root/self"}, "alice"}, + } + if got := rootTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("unexpected result. Got %v want %v", got, expected) + } +} + +func TestRemoveCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + + // Setup the command-line. + cmd := cmd_device.CmdRoot + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + deviceName := server.Status().Endpoints[0].Name() + + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"remove", "one"}); err == nil { + t.Fatalf("wrongly failed to receive a non-nil error.") + } + rootTape := tapes.ForSuffix("") + if got, expected := len(rootTape.Play()), 0; got != expected { + t.Errorf("invalid call sequence. Got %v, want %v", got, expected) + } + rootTape.Rewind() + stdout.Reset() + + rootTape.SetResponses(nil) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"associate", "remove", deviceName, "root/self"}); err != nil { + t.Fatalf("%v", err) + } + expected := []interface{}{ + AddAssociationStimulus{"AssociateAccount", []string{"root/self"}, ""}, + } + if got := rootTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("unexpected result. Got %v want %v", got, expected) + } +} diff --git a/x/ref/services/device/device/claim.go b/x/ref/services/device/device/claim.go new file mode 100644 index 000000000..150fee7dd --- /dev/null +++ b/x/ref/services/device/device/claim.go @@ -0,0 +1,68 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "encoding/base64" + "fmt" + + "v.io/v23/context" + "v.io/v23/options" + "v.io/v23/security" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" +) + +var cmdClaim = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runClaim), + Name: "claim", + Short: "Claim the device.", + Long: "Claim the device.", + ArgsName: " ", + ArgsLong: ` + is the vanadium object name of the device manager's device service. + + is used to extend the default blessing of the +current principal when blessing the app instance. + + is a token that the device manager expects to be replayed +during a claim operation on the device. + + is the marshalled public key of the device manager we +are claiming.`, +} + +func runClaim(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, max, got := 2, 4, len(args); expected > got || got > max { + return env.UsageErrorf("claim: incorrect number of arguments, expected atleast %d (max: %d), got %d", expected, max, got) + } + deviceName, grant := args[0], args[1] + var pairingToken string + if len(args) > 2 { + pairingToken = args[2] + } + var serverAuth security.Authorizer + if len(args) > 3 { + marshalledPublicKey, err := base64.URLEncoding.DecodeString(args[3]) + if err != nil { + return fmt.Errorf("Failed to base64 decode publickey: %v", err) + } + if deviceKey, err := security.UnmarshalPublicKey(marshalledPublicKey); err != nil { + return fmt.Errorf("Failed to unmarshal device public key:%v", err) + } else { + serverAuth = security.PublicKeyAuthorizer(deviceKey) + } + } else { + // Skip server endpoint authorization since an unclaimed device might + // have roots that will not be recognized by the claimer. + serverAuth = security.AllowEveryone() + } + if err := device.ClaimableClient(deviceName).Claim(ctx, pairingToken, &granter{extension: grant}, options.ServerAuthorizer{serverAuth}, options.NameResolutionAuthorizer{security.AllowEveryone()}); err != nil { + return err + } + fmt.Fprintln(env.Stdout, "Successfully claimed.") + return nil +} diff --git a/x/ref/services/device/device/claim_test.go b/x/ref/services/device/device/claim_test.go new file mode 100644 index 000000000..d8ce330b7 --- /dev/null +++ b/x/ref/services/device/device/claim_test.go @@ -0,0 +1,116 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "encoding/base64" + "reflect" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/verror" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/security" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/test" + + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" +) + +func TestClaimCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + + // Setup the command-line. + cmd := cmd_device.CmdRoot + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + deviceName := server.Status().Endpoints[0].Name() + deviceKey, err := v23.GetPrincipal(ctx).PublicKey().MarshalBinary() + if err != nil { + t.Fatalf("Failed to marshal principal public key: %v", err) + } + + // Confirm that we correctly enforce the number of arguments. + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"claim", "nope"}); err == nil { + t.Fatalf("wrongly failed to receive a non-nil error.") + } + if expected, got := "ERROR: claim: incorrect number of arguments, expected atleast 2 (max: 4), got 1", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from claim. Got %q, expected prefix %q", got, expected) + } + stdout.Reset() + stderr.Reset() + rootTape := tapes.ForSuffix("") + rootTape.Rewind() + + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"claim", "nope", "nope", "nope", "nope", "nope"}); err == nil { + t.Fatalf("wrongly failed to receive a non-nil error.") + } + if expected, got := "ERROR: claim: incorrect number of arguments, expected atleast 2 (max: 4), got 5", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from claim. Got %q, expected prefix %q", got, expected) + } + stdout.Reset() + stderr.Reset() + rootTape.Rewind() + + // Incorrect operation + var pairingToken string + var deviceKeyWrong []byte + if publicKey, _, err := security.NewPrincipalKey(); err != nil { + t.Fatalf("NewPrincipalKey failed: %v", err) + } else { + if deviceKeyWrong, err = publicKey.MarshalBinary(); err != nil { + t.Fatalf("Failed to marshal principal public key: %v", err) + } + } + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"claim", deviceName, "grant", pairingToken, base64.URLEncoding.EncodeToString(deviceKeyWrong)}); verror.ErrorID(err) != verror.ErrNotTrusted.ID { + t.Fatalf("wrongly failed to receive correct error on claim with incorrect device key:%v id:%v", err, verror.ErrorID(err)) + } + stdout.Reset() + stderr.Reset() + rootTape.Rewind() + + // Correct operation. + rootTape.SetResponses(nil) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"claim", deviceName, "grant", pairingToken, base64.URLEncoding.EncodeToString(deviceKey)}); err != nil { + t.Fatalf("Claim(%s, %s, %s) failed: %v", deviceName, "grant", pairingToken, err) + } + if got, expected := len(rootTape.Play()), 1; got != expected { + t.Errorf("invalid call sequence. Got %v, want %v", got, expected) + } + if expected, got := "Successfully claimed.", strings.TrimSpace(stdout.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from claim. Got %q, expected prefix %q", got, expected) + } + expected := []interface{}{ + "Claim", + } + if got := rootTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("unexpected result. Got %v want %v", got, expected) + } + rootTape.Rewind() + stdout.Reset() + stderr.Reset() + + // Error operation. + rootTape.SetResponses(verror.New(errOops, nil)) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"claim", deviceName, "grant", pairingToken}); err == nil { + t.Fatal("claim() failed to detect error:", err) + } + expected = []interface{}{ + "Claim", + } + if got := rootTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("unexpected result. Got %v want %v", got, expected) + } +} diff --git a/x/ref/services/device/device/debug.go b/x/ref/services/device/device/debug.go new file mode 100644 index 000000000..e91b9acdf --- /dev/null +++ b/x/ref/services/device/device/debug.go @@ -0,0 +1,38 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + "io" + "strings" + + "v.io/v23/context" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" +) + +var cmdDebug = &cmdline.Command{ + Name: "debug", + Short: "Debug the device.", + Long: "Get internal debug information about application installations and instances.", + ArgsName: "", + ArgsLong: ` + are vanadium object names or glob name patterns corresponding to application installations and instances.`, +} + +func init() { + globify(cmdDebug, runDebug, new(GlobSettings)) +} + +func runDebug(entry GlobResult, ctx *context.T, stdout, _ io.Writer) error { + if description, err := device.DeviceClient(entry.Name).Debug(ctx); err != nil { + return fmt.Errorf("Debug failed: %v", err) + } else { + line := strings.Repeat("*", len(entry.Name)+4) + fmt.Fprintf(stdout, "%s\n* %s *\n%s\n%v\n", line, entry.Name, line, description) + } + return nil +} diff --git a/x/ref/services/device/device/debug_test.go b/x/ref/services/device/device/debug_test.go new file mode 100644 index 000000000..cd1d5d64a --- /dev/null +++ b/x/ref/services/device/device/debug_test.go @@ -0,0 +1,56 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "fmt" + "reflect" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/naming" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/test" + + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" +) + +func TestDebugCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + + cmd := cmd_device.CmdRoot + addr := server.Status().Endpoints[0].String() + globName := naming.JoinAddressName(addr, "glob") + appName := naming.JoinAddressName(addr, "app") + rootTape, appTape := tapes.ForSuffix(""), tapes.ForSuffix("app") + rootTape.SetResponses(GlobResponse{results: []string{"app"}}) + + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + + debugMessage := "the secrets of the universe, revealed" + appTape.SetResponses(instanceRunning, debugMessage) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"debug", globName}); err != nil { + t.Fatalf("%v", err) + } + line := strings.Repeat("*", len(appName)+4) + expected := fmt.Sprintf("%s\n* %s *\n%s\n%s", line, appName, line, debugMessage) + if got := strings.TrimSpace(stdout.String()); got != expected { + t.Fatalf("Unexpected output from debug. Got:\n%v\nExpected:\n%v", got, expected) + } + if got, expected := appTape.Play(), []interface{}{"Status", "Debug"}; !reflect.DeepEqual(expected, got) { + t.Errorf("invalid call sequence. Got %v, want %v", got, expected) + } +} diff --git a/x/ref/services/device/device/delete.go b/x/ref/services/device/device/delete.go new file mode 100644 index 000000000..faff0dffe --- /dev/null +++ b/x/ref/services/device/device/delete.go @@ -0,0 +1,37 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + + "v.io/v23/context" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" +) + +var cmdDelete = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runDelete), + Name: "delete", + Short: "Delete the given application instance.", + Long: "Delete the given application instance.", + ArgsName: "", + ArgsLong: ` + is the vanadium object name of the application instance to delete.`, +} + +func runDelete(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 1, len(args); expected != got { + return env.UsageErrorf("delete: incorrect number of arguments, expected %d, got %d", expected, got) + } + appName := args[0] + + if err := device.ApplicationClient(appName).Delete(ctx); err != nil { + return fmt.Errorf("Delete failed: %v", err) + } + fmt.Fprintf(env.Stdout, "Delete succeeded\n") + return nil +} diff --git a/x/ref/services/device/device/delete_test.go b/x/ref/services/device/device/delete_test.go new file mode 100644 index 000000000..f1530c9b4 --- /dev/null +++ b/x/ref/services/device/device/delete_test.go @@ -0,0 +1,11 @@ +// Copyright 2015 The Vanadium 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 main_test + +import "testing" + +func TestDeleteCommand(t *testing.T) { + testHelper(t, "delete", "Delete") +} diff --git a/x/ref/services/device/device/describe.go b/x/ref/services/device/device/describe.go new file mode 100644 index 000000000..1964dbdd0 --- /dev/null +++ b/x/ref/services/device/device/describe.go @@ -0,0 +1,37 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + + "v.io/v23/context" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" +) + +var cmdDescribe = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runDescribe), + Name: "describe", + Short: "Describe the device.", + Long: "Describe the device.", + ArgsName: "", + ArgsLong: ` + is the vanadium object name of the device manager's device service.`, +} + +func runDescribe(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 1, len(args); expected != got { + return env.UsageErrorf("describe: incorrect number of arguments, expected %d, got %d", expected, got) + } + deviceName := args[0] + if description, err := device.DeviceClient(deviceName).Describe(ctx); err != nil { + return fmt.Errorf("Describe failed: %v", err) + } else { + fmt.Fprintf(env.Stdout, "%+v\n", description) + } + return nil +} diff --git a/x/ref/services/device/device/devicemanager_mock_test.go b/x/ref/services/device/device/devicemanager_mock_test.go new file mode 100644 index 000000000..4ca86c922 --- /dev/null +++ b/x/ref/services/device/device/devicemanager_mock_test.go @@ -0,0 +1,315 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "testing" + "time" + + "v.io/v23/context" + "v.io/v23/glob" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/services/binary" + "v.io/v23/services/device" + "v.io/v23/services/repository" + + "v.io/x/ref/services/internal/binarylib" + "v.io/x/ref/services/internal/packages" + "v.io/x/ref/services/internal/servicetest" +) + +type mockDeviceInvoker struct { + tape *servicetest.Tape + t *testing.T +} + +// Mock ListAssociations +type ListAssociationResponse struct { + na []device.Association + err error +} + +func (mdi *mockDeviceInvoker) ListAssociations(ctx *context.T, _ rpc.ServerCall) (associations []device.Association, err error) { + ctx.VI(2).Infof("ListAssociations() was called") + + ir := mdi.tape.Record("ListAssociations") + r := ir.(ListAssociationResponse) + return r.na, r.err +} + +// Mock AssociateAccount +type AddAssociationStimulus struct { + fun string + identityNames []string + accountName string +} + +// simpleCore implements the core of all mock methods that take +// arguments and return error. +func (mdi *mockDeviceInvoker) simpleCore(callRecord interface{}, name string) error { + ri := mdi.tape.Record(callRecord) + switch r := ri.(type) { + case nil: + return nil + case error: + return r + } + log.Fatalf("%s (mock) response %v is of bad type", name, ri) + return nil +} + +func (mdi *mockDeviceInvoker) AssociateAccount(_ *context.T, _ rpc.ServerCall, identityNames []string, accountName string) error { + return mdi.simpleCore(AddAssociationStimulus{"AssociateAccount", identityNames, accountName}, "AssociateAccount") +} + +func (mdi *mockDeviceInvoker) Claim(_ *context.T, _ rpc.ServerCall, pairingToken string) error { + return mdi.simpleCore("Claim", "Claim") +} + +func (*mockDeviceInvoker) Describe(*context.T, rpc.ServerCall) (device.Description, error) { + return device.Description{}, nil +} + +func (*mockDeviceInvoker) IsRunnable(_ *context.T, _ rpc.ServerCall, description binary.Description) (bool, error) { + return false, nil +} + +func (*mockDeviceInvoker) Reset(_ *context.T, _ rpc.ServerCall, deadline uint64) error { return nil } + +// Mock Install +type InstallStimulus struct { + fun string + appName string + config device.Config + packages application.Packages + envelope application.Envelope + // files holds a map from file or package name to file or package size. + // The app binary has the key "binary". Each of the packages will have + // the key "package/". The override packages will have the + // key "overridepackage/". + files map[string]int64 +} + +type InstallResponse struct { + appId string + err error +} + +const ( + // If provided with this app name, the mock device manager skips trying + // to fetch the envelope from the name. + appNameNoFetch = "skip-envelope-fetching" + // If provided with a fetcheable app name, the mock device manager sets + // the app name in the stimulus to this constant. + appNameAfterFetch = "envelope-fetched" + // The mock device manager sets the binary name in the envelope in the + // stimulus to this constant. + binaryNameAfterFetch = "binary-fetched" +) + +func packageSize(pkgPath string) int64 { + info, err := os.Stat(pkgPath) + if err != nil { + return -1 + } + if info.IsDir() { + infos, err := ioutil.ReadDir(pkgPath) + if err != nil { + return -1 + } + var size int64 + for _, i := range infos { + size += i.Size() + } + return size + } else { + return info.Size() + } +} + +func fetchPackageSize(ctx *context.T, pkgVON string) (int64, error) { + dir, err := ioutil.TempDir("", "package") + if err != nil { + return 0, fmt.Errorf("failed to create temp package dir: %v", err) + } + defer os.RemoveAll(dir) + tmpFile := filepath.Join(dir, "downloaded") + if err := binarylib.DownloadToFile(ctx, pkgVON, tmpFile); err != nil { + return 0, fmt.Errorf("DownloadToFile failed: %v", err) + } + dst := filepath.Join(dir, "install") + if err := packages.Install(tmpFile, dst); err != nil { + return 0, fmt.Errorf("packages.Install failed: %v", err) + } + return packageSize(dst), nil +} + +func (mdi *mockDeviceInvoker) Install(ctx *context.T, _ rpc.ServerCall, appName string, config device.Config, packages application.Packages) (string, error) { + is := InstallStimulus{"Install", appName, config, packages, application.Envelope{}, nil} + if appName != appNameNoFetch { + // Fetch the envelope and record it in the stimulus. + envelope, err := repository.ApplicationClient(appName).Match(ctx, []string{"test"}) + if err != nil { + return "", err + } + binaryName := envelope.Binary.File + envelope.Binary.File = binaryNameAfterFetch + is.appName = appNameAfterFetch + is.files = make(map[string]int64) + // Fetch the binary and record its size in the stimulus. + data, mediaInfo, err := binarylib.Download(ctx, binaryName) + if err != nil { + return "", err + } + is.files["binary"] = int64(len(data)) + if mediaInfo.Type != "application/octet-stream" { + return "", fmt.Errorf("unexpected media type: %v", mediaInfo) + } + // Iterate over the packages, download them, compute the size of + // the file(s) that make up each package, and record that in the + // stimulus. + for pkgLocalName, pkgVON := range envelope.Packages { + size, err := fetchPackageSize(ctx, pkgVON.File) + if err != nil { + return "", err + } + is.files[naming.Join("packages", pkgLocalName)] = size + } + envelope.Packages = nil + for pkgLocalName, pkg := range packages { + size, err := fetchPackageSize(ctx, pkg.File) + if err != nil { + return "", err + } + is.files[naming.Join("overridepackages", pkgLocalName)] = size + } + is.packages = nil + is.envelope = envelope + } + r := mdi.tape.Record(is).(InstallResponse) + return r.appId, r.err +} + +func (mdi *mockDeviceInvoker) Run(*context.T, rpc.ServerCall) error { + return mdi.simpleCore("Run", "Run") +} + +func (mdi *mockDeviceInvoker) Revert(*context.T, rpc.ServerCall) error { + return mdi.simpleCore("Revert", "Revert") +} + +type InstantiateResponse struct { + err error + instanceID string +} + +func (mdi *mockDeviceInvoker) Instantiate(*context.T, rpc.StreamServerCall) (string, error) { + ir := mdi.tape.Record("Instantiate") + r := ir.(InstantiateResponse) + return r.instanceID, r.err +} + +type KillStimulus struct { + fun string + delta time.Duration +} + +func (mdi *mockDeviceInvoker) Kill(_ *context.T, _ rpc.ServerCall, delta time.Duration) error { + return mdi.simpleCore(KillStimulus{"Kill", delta}, "Kill") +} + +func (mdi *mockDeviceInvoker) Delete(*context.T, rpc.ServerCall) error { + return mdi.simpleCore("Delete", "Delete") +} + +func (*mockDeviceInvoker) Uninstall(*context.T, rpc.ServerCall) error { return nil } + +func (mdi *mockDeviceInvoker) Update(*context.T, rpc.ServerCall) error { + return mdi.simpleCore("Update", "Update") +} + +func (*mockDeviceInvoker) UpdateTo(*context.T, rpc.ServerCall, string) error { return nil } + +// Mock Permissions getting and setting +type GetPermissionsResponse struct { + perms access.Permissions + version string + err error +} + +type SetPermissionsStimulus struct { + fun string + perms access.Permissions + version string +} + +func (mdi *mockDeviceInvoker) SetPermissions(_ *context.T, _ rpc.ServerCall, perms access.Permissions, version string) error { + return mdi.simpleCore(SetPermissionsStimulus{"SetPermissions", perms, version}, "SetPermissions") +} + +func (mdi *mockDeviceInvoker) GetPermissions(*context.T, rpc.ServerCall) (access.Permissions, string, error) { + ir := mdi.tape.Record("GetPermissions") + r := ir.(GetPermissionsResponse) + return r.perms, r.version, r.err +} + +func (mdi *mockDeviceInvoker) Debug(*context.T, rpc.ServerCall) (string, error) { + ir := mdi.tape.Record("Debug") + r := ir.(string) + return r, nil +} + +func (mdi *mockDeviceInvoker) Status(*context.T, rpc.ServerCall) (device.Status, error) { + ir := mdi.tape.Record("Status") + switch r := ir.(type) { + case device.Status: + return r, nil + case error: + return nil, r + default: + log.Fatalf("Status (mock) response %v is of bad type", ir) + return nil, nil + } +} + +type GlobStimulus struct { + pattern string +} + +type GlobResponse struct { + results []string + err error +} + +func (mdi *mockDeviceInvoker) Glob__(_ *context.T, call rpc.GlobServerCall, g *glob.Glob) error { + gs := GlobStimulus{g.String()} + gr := mdi.tape.Record(gs).(GlobResponse) + for _, r := range gr.results { + call.SendStream().Send(naming.GlobReplyEntry{Value: naming.MountEntry{Name: r}}) + } + return gr.err +} + +type dispatcher struct { + tapes *servicetest.TapeMap + t *testing.T +} + +func newDispatcher(t *testing.T, tapes *servicetest.TapeMap) rpc.Dispatcher { + return &dispatcher{tapes: tapes, t: t} +} + +func (d *dispatcher) Lookup(_ *context.T, suffix string) (interface{}, security.Authorizer, error) { + return &mockDeviceInvoker{tape: d.tapes.ForSuffix(suffix), t: d.t}, nil, nil +} diff --git a/x/ref/services/device/device/doc.go b/x/ref/services/device/device/doc.go new file mode 100644 index 000000000..29d73338d --- /dev/null +++ b/x/ref/services/device/device/doc.go @@ -0,0 +1,523 @@ +// Copyright 2018 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated via go generate. +// DO NOT UPDATE MANUALLY + +/* +Command device facilitates interaction with the Vanadium device manager. + +Usage: + device [flags] + +The device commands are: + install Install the given application. + install-local Install the given application from the local system. + uninstall Uninstall the given application installation. + associate Tool for creating associations between Vanadium blessings and a + system account + describe Describe the device. + claim Claim the device. + instantiate Create an instance of the given application. + delete Delete the given application instance. + run Run the given application instance. + kill Kill the given application instance. + revert Revert the device manager or applications. + update Update the device manager or applications. + status Get device manager or application status. + debug Debug the device. + acl Tool for setting device manager Permissions + publish Publish the given application(s). + ls List applications. + help Display help for commands or topics + +The global flags are: + -v23.namespace.root=[/(dev.v.io:r:vprod:service:mounttabled)@ns.dev.v.io:8101] + local namespace root; can be repeated to provided multiple roots + -v23.proxy= + object name of proxy service to use to export services across network + boundaries + + -alsologtostderr=true + log to standard error as well as files + -log_backtrace_at=:0 + when logging hits line file:N, emit a stack trace + -log_dir= + if non-empty, write log files to this directory + -logtostderr=false + log to standard error instead of files + -max_stack_buf_size=4292608 + max size in bytes of the buffer to use for logging stack traces + -metadata= + Displays metadata for the program and exits. + -stderrthreshold=2 + logs at or above this threshold go to stderr + -time=false + Dump timing information to stderr before exiting the program. + -v=0 + log level for V logs + -v23.credentials= + directory to use for storing security credentials + -v23.i18n-catalogue= + 18n catalogue files to load, comma separated + -v23.permissions.file= + specify a perms file as : + -v23.permissions.literal= + explicitly specify the runtime perms as a JSON-encoded access.Permissions. + Overrides all --v23.permissions.file flags + -v23.tcp.address= + address to listen on + -v23.tcp.protocol= + protocol to listen with + -v23.vtrace.cache-size=1024 + The number of vtrace traces to store in memory + -v23.vtrace.collect-regexp= + Spans and annotations that match this regular expression will trigger trace + collection + -v23.vtrace.dump-on-shutdown=true + If true, dump all stored traces on runtime shutdown + -v23.vtrace.sample-rate=0 + Rate (from 0.0 to 1.0) to sample vtrace traces + -v23.vtrace.v=0 + The verbosity level of the log messages to be captured in traces + -vmodule= + comma-separated list of globpattern=N settings for filename-filtered logging + (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns baz or + *az or b* but not by bar/baz or baz.go or az or b.* + -vpath= + comma-separated list of regexppattern=N settings for file pathname-filtered + logging (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns + foo/bar/baz or fo.*az or oo/ba or b.z but not by foo/bar/baz.go or fo*az + +Device install + +Install the given application and print the name of the new installation. + +Usage: + device install [flags] + + is the vanadium object name of the device manager's app service. + + is the vanadium object name of the application. + +The device install flags are: + -config={} + JSON-encoded device.Config object, of the form: + '{"flag1":"value1","flag2":"value2"}' + -packages={} + JSON-encoded application.Packages object, of the form: + '{"pkg1":{"File":"object name 1"},"pkg2":{"File":"object name 2"}}' + +Device install-local + +Install the given application specified using a local path, and print the name +of the new installation. + +Usage: + device install-local [flags] [ENV=VAL ...] binary [--flag=val ...] [PACKAGES path ...] + +<device> is the vanadium object name of the device manager's app service. + +<title> is the app title. + +This is followed by an arbitrary number of environment variable settings, the +local path for the binary to install, and arbitrary flag settings and args. +Optionally, this can be followed by 'PACKAGES' and a list of local files and +directories to be installed as packages for the app + +The device install-local flags are: + -config={} + JSON-encoded device.Config object, of the form: + '{"flag1":"value1","flag2":"value2"}' + -packages={} + JSON-encoded application.Packages object, of the form: + '{"pkg1":{"File":"local file path1"},"pkg2":{"File":"local file path 2"}}' + +Device uninstall + +Uninstall the given application installation. + +Usage: + device uninstall [flags] <installation> + +<installation> is the vanadium object name of the application installation to +uninstall. + +Device associate - Tool for creating associations between Vanadium blessings and a system account + +The associate tool facilitates managing blessing to system account associations. + +Usage: + device associate [flags] <command> + +The device associate commands are: + list Lists the account associations. + add Add the listed blessings with the specified system account. + remove Removes system accounts associated with the listed blessings. + +Device associate list + +Lists all account associations. + +Usage: + device associate list [flags] <devicemanager>. + +<devicemanager> is the name of the device manager to connect to. + +Device associate add + +Add the listed blessings with the specified system account. + +Usage: + device associate add [flags] <devicemanager> <systemName> <blessing>... + +<devicemanager> is the name of the device manager to connect to. <systemName> is +the name of an account holder on the local system. <blessing>.. are the +blessings to associate systemAccount with. + +Device associate remove + +Removes system accounts associated with the listed blessings. + +Usage: + device associate remove [flags] <devicemanager> <blessing>... + +<devicemanager> is the name of the device manager to connect to. <blessing>... +is a list of blessings. + +Device describe + +Describe the device. + +Usage: + device describe [flags] <device> + +<device> is the vanadium object name of the device manager's device service. + +Device claim + +Claim the device. + +Usage: + device claim [flags] <device> <grant extension> <pairing token> <device publickey> + +<device> is the vanadium object name of the device manager's device service. + +<grant extension> is used to extend the default blessing of the current +principal when blessing the app instance. + +<pairing token> is a token that the device manager expects to be replayed during +a claim operation on the device. + +<device publickey> is the marshalled public key of the device manager we are +claiming. + +Device instantiate + +Create an instance of the given application, provide it with a blessing, and +print the name of the new instance. + +Usage: + device instantiate [flags] <application installation> <grant extension> + +<application installation> is the vanadium object name of the application +installation from which to create an instance. + +<grant extension> is used to extend the default blessing of the current +principal when blessing the app instance. + +Device delete + +Delete the given application instance. + +Usage: + device delete [flags] <app instance> + +<app instance> is the vanadium object name of the application instance to +delete. + +Device run + +Run the given application instance. + +Usage: + device run [flags] <app instance> + +<app instance> is the vanadium object name of the application instance to run. + +Device kill + +Kill the given application instance. + +Usage: + device kill [flags] <app instance> + +<app instance> is the vanadium object name of the application instance to kill. + +Device revert + +Revert the device manager or application instances and installations to a +previous version of their current version + +Usage: + device revert [flags] <name patterns...> + +<name patterns...> are vanadium object names or glob name patterns corresponding +to the device manager service, or to application installations and instances. + +The device revert flags are: + -installation-state=!Uninstalled + If non-empty, specifies allowed installation states (all others installations + get filtered out). The value of the flag is a comma-separated list of values + from among: [Active Uninstalled]. If the value is prefixed by '!', the list + acts as a blacklist (all matching installations get filtered out). + -instance-state=!Deleted + If non-empty, specifies allowed instance states (all other instances get + filtered out). The value of the flag is a comma-separated list of values from + among: [Launching Running Dying NotRunning Updating Deleted]. If the value is + prefixed by '!', the list acts as a blacklist (all matching instances get + filtered out). + -only-installations=false + If set, only consider installations. + -only-instances=false + If set, only consider instances. + -parallelism=BYKIND + Specifies the level of parallelism for the handler execution. One of: [BYKIND + FULL NONE]. + +Device update + +Update the device manager or application instances and installations + +Usage: + device update [flags] <name patterns...> + +<name patterns...> are vanadium object names or glob name patterns corresponding +to the device manager service, or to application installations and instances. + +The device update flags are: + -installation-state=!Uninstalled + If non-empty, specifies allowed installation states (all others installations + get filtered out). The value of the flag is a comma-separated list of values + from among: [Active Uninstalled]. If the value is prefixed by '!', the list + acts as a blacklist (all matching installations get filtered out). + -instance-state=!Deleted + If non-empty, specifies allowed instance states (all other instances get + filtered out). The value of the flag is a comma-separated list of values from + among: [Launching Running Dying NotRunning Updating Deleted]. If the value is + prefixed by '!', the list acts as a blacklist (all matching instances get + filtered out). + -only-installations=false + If set, only consider installations. + -only-instances=false + If set, only consider instances. + -parallelism=BYKIND + Specifies the level of parallelism for the handler execution. One of: [BYKIND + FULL NONE]. + +Device status + +Get the status of the device manager or application instances and installations. + +Usage: + device status [flags] <name patterns...> + +<name patterns...> are vanadium object names or glob name patterns corresponding +to the device manager service, or to application installations and instances. + +The device status flags are: + -installation-state= + If non-empty, specifies allowed installation states (all others installations + get filtered out). The value of the flag is a comma-separated list of values + from among: [Active Uninstalled]. If the value is prefixed by '!', the list + acts as a blacklist (all matching installations get filtered out). + -instance-state= + If non-empty, specifies allowed instance states (all other instances get + filtered out). The value of the flag is a comma-separated list of values from + among: [Launching Running Dying NotRunning Updating Deleted]. If the value is + prefixed by '!', the list acts as a blacklist (all matching instances get + filtered out). + -only-installations=false + If set, only consider installations. + -only-instances=false + If set, only consider instances. + -parallelism=FULL + Specifies the level of parallelism for the handler execution. One of: [BYKIND + FULL NONE]. + +Device debug + +Get internal debug information about application installations and instances. + +Usage: + device debug [flags] <app name patterns...> + +<app name patterns...> are vanadium object names or glob name patterns +corresponding to application installations and instances. + +The device debug flags are: + -installation-state= + If non-empty, specifies allowed installation states (all others installations + get filtered out). The value of the flag is a comma-separated list of values + from among: [Active Uninstalled]. If the value is prefixed by '!', the list + acts as a blacklist (all matching installations get filtered out). + -instance-state= + If non-empty, specifies allowed instance states (all other instances get + filtered out). The value of the flag is a comma-separated list of values from + among: [Launching Running Dying NotRunning Updating Deleted]. If the value is + prefixed by '!', the list acts as a blacklist (all matching instances get + filtered out). + -only-installations=false + If set, only consider installations. + -only-instances=false + If set, only consider instances. + -parallelism=FULL + Specifies the level of parallelism for the handler execution. One of: [BYKIND + FULL NONE]. + +Device acl - Tool for setting device manager Permissions + +The acl tool manages Permissions on the device manger, installations and +instances. + +Usage: + device acl [flags] <command> + +The device acl commands are: + get Get Permissions for the given target. + set Set Permissions for the given target. + +Device acl get + +Get Permissions for the given target. + +Usage: + device acl get [flags] <device manager name> + +<device manager name> can be a Vanadium name for a device manager, application +installation or instance. + +Device acl set + +Set Permissions for the given target + +Usage: + device acl set [flags] <device manager name> (<blessing> [!]<tag>(,[!]<tag>)* + +<device manager name> can be a Vanadium name for a device manager, application +installation or instance. + +<blessing> is a blessing pattern. If the same pattern is repeated multiple times +in the command, then the only the last occurrence will be honored. + +<tag> is a subset of defined access types ("Admin", "Read", "Write" etc.). If +the access right is prefixed with a '!' then <blessing> is added to the NotIn +list for that right. Using "^" as a "tag" causes all occurrences of <blessing> +in the current AccessList to be cleared. + +Examples: set root/self ^ will remove "root/self" from the In and NotIn lists +for all access rights. + +set root/self Read,!Write will add "root/self" to the In list for Read access +and the NotIn list for Write access (and remove "root/self" from both the In and +NotIn lists of all other access rights) + +The device acl set flags are: + -f=false + Instead of making the AccessLists additive, do a complete replacement based + on the specified settings. + +Device publish + +Publishes the given application(s) to the binary and application servers. The +binaries should be in $JIRI_ROOT/release/go/bin/[<GOOS>_<GOARCH>] by default +(can be overrriden with --from). By default the binary name is used as the name +of the application envelope, and as the title in the envelope. However, +<envelope-name> and <title> can be specified explicitly using :<envelope-name> +and @<title>. The binary is published as <binserv>/<binary +name>/<GOOS>-<GOARCH>/<TIMESTAMP>. The application envelope is published as +<appserv>/<envelope-name>/<TIMESTAMP>. Optionally, adds blessing patterns to the +Read and Resolve AccessLists. + +Usage: + device publish [flags] <binary name>[:<envelope-name>][@<title>] ... + +The device publish flags are: + -add-publisher=true + If true, add a publisher blessing to the application envelope + -appserv=applications + Name of application service. + -binserv=binaries + Name of binary service. + -from= + Location of binaries to be published. Defaults to + $JIRI_ROOT/release/go/bin/[<GOOS>_<GOARCH>] + -goarch=<runtime.GOARCH> + GOARCH for application. The default is the value of runtime.GOARCH. + -goos=<runtime.GOOS> + GOOS for application. The default is the value of runtime.GOOS. + -publisher-min-validity=30h0m0s + Publisher blessings that are valid for less than this amount of time are + considered invalid + -readers=dev.v.io + If non-empty, comma-separated blessing patterns to add to Read and Resolve + AccessList. + +Device ls + +List application installations or instances. + +Usage: + device ls [flags] <app name patterns...> + +<app name patterns...> are vanadium object names or glob name patterns +corresponding to application installations and instances. + +The device ls flags are: + -installation-state= + If non-empty, specifies allowed installation states (all others installations + get filtered out). The value of the flag is a comma-separated list of values + from among: [Active Uninstalled]. If the value is prefixed by '!', the list + acts as a blacklist (all matching installations get filtered out). + -instance-state= + If non-empty, specifies allowed instance states (all other instances get + filtered out). The value of the flag is a comma-separated list of values from + among: [Launching Running Dying NotRunning Updating Deleted]. If the value is + prefixed by '!', the list acts as a blacklist (all matching instances get + filtered out). + -only-installations=false + If set, only consider installations. + -only-instances=false + If set, only consider instances. + -parallelism=FULL + Specifies the level of parallelism for the handler execution. One of: [BYKIND + FULL NONE]. + +Device help - Display help for commands or topics + +Help with no args displays the usage of the parent command. + +Help with args displays the usage of the specified sub-command or help topic. + +"help ..." recursively displays help for all commands and topics. + +Usage: + device help [flags] [command/topic ...] + +[command/topic ...] optionally identifies a specific sub-command or help topic. + +The device help flags are: + -style=compact + The formatting style for help output: + compact - Good for compact cmdline output. + full - Good for cmdline output, shows all global flags. + godoc - Good for godoc processing. + shortonly - Only output short description. + Override the default by setting the CMDLINE_STYLE environment variable. + -width=<terminal width> + Format output to this target width in runes, or unlimited if width < 0. + Defaults to the terminal width if available. Override the default by setting + the CMDLINE_WIDTH environment variable. +*/ +package main diff --git a/x/ref/services/device/device/glob.go b/x/ref/services/device/device/glob.go new file mode 100644 index 000000000..2848aa205 --- /dev/null +++ b/x/ref/services/device/device/glob.go @@ -0,0 +1,505 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "bytes" + "flag" + "fmt" + "io" + "regexp" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/services/device" + "v.io/v23/verror" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/services/device/internal/errors" +) + +// GlobHandler is implemented by each command that wants to execute against name +// patterns. The handler is expected to be invoked against each glob result, +// and can be run concurrently. The handler should direct its output to the +// given stdout and stderr writers. +// +// There are three usage patterns, depending on the desired level of control +// over the execution flow and settings manipulation. +// +// (1) Most control +// +// func myCmdHandler(entry GlobResult, ctx *context.T, stdout, stderr io.Writer) error { +// output := myCmdProcessing(entry) +// fmt.Fprintf(stdout, output) +// ... +// } +// +// func runMyCmd(ctx *context.T, env *cmdline.Env, args []string) error { +// ... +// err := Run(ctx, env, args, myCmdHandler, GlobSettings{}) +// ... +// } +// +// var cmdMyCmd = &cmdline.Command { +// Runner: v23cmd.RunnerFunc(runMyCmd) +// ... +// } +// +// (2) Pre-packaged runner +// +// If all runMyCmd does is to call Run, you can use globRunner to avoid having +// to define runMyCmd: +// +// var cmdMyCmd = &cmdline.Command { +// Runner: globRunner(myCmdHandler, &GlobSettings{}), +// Name: "mycmd", +// ... +// } +// +// (3) Pre-packaged runner and glob settings flag configuration +// +// If, additionally, you want the GlobSettings to be configurable with +// command-line flags, you can use globify instead: +// +// var cmdMyCmd = &cmdline.Command { +// Name: "mycmd", +// ... +// } +// +// func init() { +// globify(cmdMyCmd, myCmdHandler, &GlobSettings{}), +// } +// +// The GlobHandler identifier is exported for use in unit tests. +type GlobHandler func(entry GlobResult, ctx *context.T, stdout, stderr io.Writer) error + +func globRunner(handler GlobHandler, s *GlobSettings) cmdline.Runner { + return v23cmd.RunnerFunc(func(ctx *context.T, env *cmdline.Env, args []string) error { + return Run(ctx, env, args, handler, *s) + }) +} + +type objectKind int + +const ( + ApplicationInstallationObject objectKind = iota + ApplicationInstanceObject + DeviceServiceObject + SentinelObjectKind // For invariant checking in testing. +) + +var ObjectKinds = []objectKind{ + ApplicationInstallationObject, + ApplicationInstanceObject, + DeviceServiceObject, +} + +// GlobResult defines the input to a GlobHandler. +// The identifier is exported for use in unit tests. +type GlobResult struct { + Name string + Status device.Status + Kind objectKind +} + +func NewGlobResult(name string, status device.Status) (*GlobResult, error) { + var kind objectKind + switch status.(type) { + case device.StatusInstallation: + kind = ApplicationInstallationObject + case device.StatusInstance: + kind = ApplicationInstanceObject + case device.StatusDevice: + kind = DeviceServiceObject + default: + return nil, fmt.Errorf("Status(%v) returned unrecognized status type %T\n", name, status) + } + return &GlobResult{ + Name: name, + Status: status, + Kind: kind, + }, nil +} + +type byTypeAndName []*GlobResult + +func (a byTypeAndName) Len() int { return len(a) } +func (a byTypeAndName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +func (a byTypeAndName) Less(i, j int) bool { + r1, r2 := a[i], a[j] + if r1.Kind != r2.Kind { + return r1.Kind < r2.Kind + } + return r1.Name < r2.Name +} + +// Run runs the given handler in parallel against each of the results obtained +// by globbing args, after performing filtering based on type +// (instance/installation) and state. No de-duping of results is performed. +// The outputs from each of the handlers are sorted: installations first, then +// instances (and alphabetically by object name for each group). +// The identifier is exported for use in unit tests. +func Run(ctx *context.T, env *cmdline.Env, args []string, handler GlobHandler, s GlobSettings) error { + results := glob(ctx, env, args) + sort.Sort(byTypeAndName(results)) + results = filterResults(results, s) + if len(results) == 0 { + return fmt.Errorf("no objects found") + } + stdouts, stderrs := make([]bytes.Buffer, len(results)), make([]bytes.Buffer, len(results)) + var errorCounter uint32 = 0 + perResult := func(r *GlobResult, index int) { + if err := handler(*r, ctx, &stdouts[index], &stderrs[index]); err != nil { + fmt.Fprintf(&stderrs[index], "ERROR for \"%s\": %v.\n", r.Name, err) + atomic.AddUint32(&errorCounter, 1) + } + } + switch s.HandlerParallelism { + case FullParallelism: + var wg sync.WaitGroup + for i, r := range results { + wg.Add(1) + go func(r *GlobResult, i int) { + perResult(r, i) + wg.Done() + }(r, i) + } + wg.Wait() + case NoParallelism: + for i, r := range results { + perResult(r, i) + } + case KindParallelism: + processed := 0 + for _, k := range ObjectKinds { + var wg sync.WaitGroup + for i, r := range results { + if r.Kind != k { + continue + } + wg.Add(1) + processed++ + go func(r *GlobResult, i int) { + perResult(r, i) + wg.Done() + }(r, i) + } + wg.Wait() + } + if processed != len(results) { + return fmt.Errorf("broken invariant: unhandled object kind") + } + } + for i := range results { + io.Copy(env.Stdout, &stdouts[i]) + io.Copy(env.Stderr, &stderrs[i]) + } + if errorCounter > 0 { + return fmt.Errorf("encountered a total of %d error(s)", errorCounter) + } + return nil +} + +func filterResults(results []*GlobResult, s GlobSettings) []*GlobResult { + var ret []*GlobResult + for _, r := range results { + switch status := r.Status.(type) { + case device.StatusInstance: + if s.OnlyInstallations || !s.InstanceStateFilter.apply(status.Value.State) { + continue + } + case device.StatusInstallation: + if s.OnlyInstances || !s.InstallationStateFilter.apply(status.Value.State) { + continue + } + case device.StatusDevice: + if s.OnlyInstances || s.OnlyInstallations { + continue + } + } + ret = append(ret, r) + } + return ret +} + +// TODO(caprita): We need to filter out debug objects under the app instances' +// namespaces, so that the tool works with ... patterns. We should change glob +// on device manager to not return debug objects by default for apps and instead +// put them under a __debug suffix (like it works for services). +var debugNameRE = regexp.MustCompile("/apps/[^/]+/[^/]+/[^/]+/(logs|stats|pprof)(/|$)") + +func getStatus(ctx *context.T, env *cmdline.Env, name string, resultsCh chan<- *GlobResult) { + status, err := device.DeviceClient(name).Status(ctx) + // Skip non-instances/installations. + if verror.ErrorID(err) == errors.ErrInvalidSuffix.ID { + return + } + if err != nil { + fmt.Fprintf(env.Stderr, "Status(%v) failed: %v\n", name, err) + return + } + if r, err := NewGlobResult(name, status); err != nil { + fmt.Fprintf(env.Stderr, "%v\n", err) + } else { + resultsCh <- r + } +} + +func globOne(ctx *context.T, env *cmdline.Env, pattern string, resultsCh chan<- *GlobResult) { + globCh, err := v23.GetNamespace(ctx).Glob(ctx, pattern) + if err != nil { + fmt.Fprintf(env.Stderr, "Glob(%v) failed: %v\n", pattern, err) + return + } + var wg sync.WaitGroup + // For each glob result. + for entry := range globCh { + switch v := entry.(type) { + case *naming.GlobReplyEntry: + name := v.Value.Name + // Skip debug objects. + if debugNameRE.MatchString(name) { + continue + } + wg.Add(1) + go func() { + getStatus(ctx, env, name, resultsCh) + wg.Done() + }() + case *naming.GlobReplyError: + fmt.Fprintf(env.Stderr, "Glob(%v) returned an error for %v: %v\n", pattern, v.Value.Name, v.Value.Error) + } + } + wg.Wait() +} + +// glob globs the given arguments and returns the results in arbitrary order. +func glob(ctx *context.T, env *cmdline.Env, args []string) []*GlobResult { + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + var wg sync.WaitGroup + resultsCh := make(chan *GlobResult, 100) + // For each pattern. + for _, a := range args { + wg.Add(1) + go func(pattern string) { + globOne(ctx, env, pattern, resultsCh) + wg.Done() + }(a) + } + // Collect the glob results into a slice. + var results []*GlobResult + done := make(chan struct{}) + go func() { + for r := range resultsCh { + results = append(results, r) + } + close(done) + }() + wg.Wait() + close(resultsCh) + <-done + return results +} + +type genericStateFlag struct { + set map[genericState]bool + exclude bool +} + +// genericState interface is meant to abstract device.InstanceState and +// device.InstallationState. We only make use of the String method, but we +// could also add Set and VDLReflect to the method set to constrain the types +// that can be used. Ultimately, however, the state constructor passed into +// genericStateFlag.fromString is the gatekeeper as to what can be allowed in +// the genericStateFlag set. +type genericState fmt.Stringer + +func (f *genericStateFlag) apply(state genericState) bool { + if len(f.set) == 0 { + return true + } + return f.exclude != f.set[state] +} + +func (f *genericStateFlag) String() string { + states := make([]string, 0, len(f.set)) + for s := range f.set { + states = append(states, s.String()) + } + sort.Strings(states) + statesStr := strings.Join(states, ",") + if f.exclude { + return "!" + statesStr + } + return statesStr +} + +func (f *genericStateFlag) fromString(s string, stateConstructor func(string) (genericState, error)) error { + if len(s) > 0 && s[0] == '!' { + f.exclude = true + s = s[1:] + } + states := strings.Split(s, ",") + for _, s := range states { + state, err := stateConstructor(s) + if err != nil { + return err + } + f.add(state) + } + return nil +} + +func (f *genericStateFlag) add(s genericState) { + if f.set == nil { + f.set = make(map[genericState]bool) + } + f.set[s] = true +} + +type instanceStateFlag struct { + genericStateFlag +} + +func (f *instanceStateFlag) Set(s string) error { + return f.fromString(s, func(s string) (genericState, error) { + return device.InstanceStateFromString(s) + }) +} + +func InstanceStates(states ...device.InstanceState) (f instanceStateFlag) { + for _, s := range states { + f.add(s) + } + return +} + +func ExcludeInstanceStates(states ...device.InstanceState) instanceStateFlag { + f := InstanceStates(states...) + f.exclude = true + return f +} + +type installationStateFlag struct { + genericStateFlag +} + +func (f *installationStateFlag) Set(s string) error { + return f.fromString(s, func(s string) (genericState, error) { + return device.InstallationStateFromString(s) + }) +} + +func InstallationStates(states ...device.InstallationState) (f installationStateFlag) { + for _, s := range states { + f.add(s) + } + return +} + +func ExcludeInstallationStates(states ...device.InstallationState) installationStateFlag { + f := InstallationStates(states...) + f.exclude = true + return f +} + +type parallelismFlag int + +const ( + FullParallelism parallelismFlag = iota + NoParallelism + KindParallelism + sentinelParallelismFlag +) + +var parallelismStrings = map[parallelismFlag]string{ + FullParallelism: "FULL", + NoParallelism: "NONE", + KindParallelism: "BYKIND", +} + +func init() { + if len(parallelismStrings) != int(sentinelParallelismFlag) { + panic(fmt.Sprintf("broken invariant: mismatching number of parallelism types")) + } +} + +func (p *parallelismFlag) String() string { + s, ok := parallelismStrings[*p] + if !ok { + return "UNKNOWN" + } + return s +} + +func (p *parallelismFlag) Set(s string) error { + for k, v := range parallelismStrings { + if s == v { + *p = k + return nil + } + } + return fmt.Errorf("unrecognized parallelism type: %v", s) +} + +// GlobSettings specifies flag-settable options and filters for globbing. +// The identifier is exported for use in unit tests. +type GlobSettings struct { + InstanceStateFilter instanceStateFlag + InstallationStateFilter installationStateFlag + OnlyInstances bool + OnlyInstallations bool + HandlerParallelism parallelismFlag + defaults *GlobSettings +} + +func (s *GlobSettings) reset() { + d := s.defaults + *s = *d + s.defaults = d +} + +func (s *GlobSettings) setDefaults(d GlobSettings) { + s.defaults = new(GlobSettings) + *s.defaults = d +} + +var allGlobSettings []*GlobSettings + +// ResetGlobSettings is meant for tests to restore the values of flag-configured +// variables when running multiple commands in the same process. +func ResetGlobSettings() { + for _, s := range allGlobSettings { + s.reset() + } +} + +func defineGlobFlags(fs *flag.FlagSet, s *GlobSettings) { + fs.Var(&s.InstanceStateFilter, "instance-state", fmt.Sprintf("If non-empty, specifies allowed instance states (all other instances get filtered out). The value of the flag is a comma-separated list of values from among: %v. If the value is prefixed by '!', the list acts as a blacklist (all matching instances get filtered out).", device.InstanceStateAll)) + fs.Var(&s.InstallationStateFilter, "installation-state", fmt.Sprintf("If non-empty, specifies allowed installation states (all others installations get filtered out). The value of the flag is a comma-separated list of values from among: %v. If the value is prefixed by '!', the list acts as a blacklist (all matching installations get filtered out).", device.InstallationStateAll)) + fs.BoolVar(&s.OnlyInstances, "only-instances", false, "If set, only consider instances.") + fs.BoolVar(&s.OnlyInstallations, "only-installations", false, "If set, only consider installations.") + var parallelismValues []string + for _, v := range parallelismStrings { + parallelismValues = append(parallelismValues, v) + } + sort.Strings(parallelismValues) + fs.Var(&s.HandlerParallelism, "parallelism", fmt.Sprintf("Specifies the level of parallelism for the handler execution. One of: %v.", parallelismValues)) +} + +func globify(c *cmdline.Command, handler GlobHandler, s *GlobSettings) { + s.setDefaults(*s) + defineGlobFlags(&c.Flags, s) + c.Runner = globRunner(handler, s) + allGlobSettings = append(allGlobSettings, s) +} diff --git a/x/ref/services/device/device/glob_test.go b/x/ref/services/device/device/glob_test.go new file mode 100644 index 000000000..4c3bb2542 --- /dev/null +++ b/x/ref/services/device/device/glob_test.go @@ -0,0 +1,527 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "fmt" + "io" + "math/rand" + "runtime" + "strings" + "sync" + "testing" + "time" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/test" +) + +func simplePrintHandler(entry cmd_device.GlobResult, _ *context.T, stdout, _ io.Writer) error { + fmt.Fprintf(stdout, "%v\n", entry) + return nil +} + +func errOnInstallationsHandler(entry cmd_device.GlobResult, _ *context.T, stdout, _ io.Writer) error { + if entry.Kind == cmd_device.ApplicationInstallationObject { + return fmt.Errorf("handler complete failure") + } + fmt.Fprintf(stdout, "%v\n", entry) + return nil +} + +// newEnforceFullParallelismHandler returns a handler that should be invoked in +// parallel n times. +func newEnforceFullParallelismHandler(t *testing.T, n int) cmd_device.GlobHandler { + // The WaitGroup is used to verify parallel execution: each run of the + // handler decrements the counter, and then waits for the counter to + // reach zero. If any of the runs of the handler were sequential, the + // counter would never reach zero since a deadlock would ensue. A + // timeout protects against the deadlock to ensure the test fails fast. + var wg sync.WaitGroup + wg.Add(n) + return func(entry cmd_device.GlobResult, ctx *context.T, stdout, stderr io.Writer) error { + wg.Done() + simplePrintHandler(entry, ctx, stdout, stderr) + waitDoneCh := make(chan struct{}) + go func() { + wg.Wait() + close(waitDoneCh) + }() + select { + case <-waitDoneCh: + case <-time.After(5 * time.Second): + t.Errorf("Timed out waiting for WaitGroup. Potential parallelism issue.") + } + return nil + } +} + +// maybeGosched flips a coin to decide where to call Gosched. It's used to +// shuffle up the order of handler execution a bit (to prevent the goroutines +// spawned by the glob library from always executing in a fixed order, e.g. the +// order in which they're created). +func maybeGosched() { + if rand.Intn(2) == 0 { + runtime.Gosched() + } +} + +// newEnforceKindParallelismHandler returns a handler that should be invoked +// nInstallations times in parallel for installations, then nInstances times in +// parallel for instances, then nDevices times in parallel for device service +// objects. +func newEnforceKindParallelismHandler(t *testing.T, nInstallations, nInstances, nDevices int) cmd_device.GlobHandler { + // Each of these handlers ensures parallelism within each kind + // (installations, instances, device objects). + hInstallations := newEnforceFullParallelismHandler(t, nInstallations) + hInstances := newEnforceFullParallelismHandler(t, nInstances) + hDevices := newEnforceFullParallelismHandler(t, nDevices) + + // These channels are used to verify that all installation handlers must + // execute before all instance handlers, which in turn must execute + // before all device handlers. + instancesCh := make(chan struct{}, nInstances) + devicesCh := make(chan struct{}, nDevices) + return func(entry cmd_device.GlobResult, ctx *context.T, stdout, stderr io.Writer) error { + maybeGosched() + switch entry.Kind { + case cmd_device.ApplicationInstallationObject: + select { + case <-instancesCh: + t.Errorf("Instance before installation") + case <-devicesCh: + t.Errorf("Device before installation") + default: + } + return hInstallations(entry, ctx, stdout, stderr) + case cmd_device.ApplicationInstanceObject: + select { + case <-devicesCh: + t.Errorf("Device before instance") + default: + } + instancesCh <- struct{}{} + return hInstances(entry, ctx, stdout, stderr) + case cmd_device.DeviceServiceObject: + devicesCh <- struct{}{} + return hDevices(entry, ctx, stdout, stderr) + } + t.Errorf("Unknown entry: %v", entry.Kind) + return nil + } +} + +// newEnforceNoParallelismHandler returns a handler meant to be invoked sequentially +// for each of the suffixes contained in the expected slice. +func newEnforceNoParallelismHandler(t *testing.T, n int, expected []string) cmd_device.GlobHandler { + if n != len(expected) { + t.Errorf("Test invariant broken: %d != %d", n, len(expected)) + } + orderedSuffixes := make(chan string, n) + for _, e := range expected { + orderedSuffixes <- e + } + return func(entry cmd_device.GlobResult, ctx *context.T, stdout, stderr io.Writer) error { + maybeGosched() + _, suffix := naming.SplitAddressName(entry.Name) + expect := <-orderedSuffixes + if suffix != expect { + t.Errorf("Expected %s, got %s", expect, suffix) + } + simplePrintHandler(entry, ctx, stdout, stderr) + return nil + } +} + +// TestObjectKindInvariant ensures that the object kind enum and list are in +// sync and have not been inadvertently updated independently of each other. +func TestObjectKindInvariant(t *testing.T) { + if len(cmd_device.ObjectKinds) != int(cmd_device.SentinelObjectKind) { + t.Errorf("Broken invariant: mismatching number of object kinds") + } +} + +// TestGlob tests the internals of the globbing support for the device tool. +func TestGlob(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + tapes := servicetest.NewTapeMap() + rootTape := tapes.ForSuffix("") + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + endpoint := server.Status().Endpoints[0] + appName := naming.JoinAddressName(endpoint.String(), "app") + + allGlobArgs := []string{"glob1", "glob2"} + allGlobResponses := []GlobResponse{ + {results: []string{"app/3", "app/4", "app/6", "app/5", "app/9", "app/7"}}, + {results: []string{"app/2", "app/1", "app/8"}}, + } + allStatusResponses := map[string][]interface{}{ + "app/1": []interface{}{instanceRunning}, + "app/2": []interface{}{installationUninstalled}, + "app/3": []interface{}{instanceUpdating}, + "app/4": []interface{}{installationActive}, + "app/5": []interface{}{instanceNotRunning}, + "app/6": []interface{}{deviceService}, + "app/7": []interface{}{installationActive}, + "app/8": []interface{}{deviceUpdating}, + "app/9": []interface{}{instanceUpdating}, + } + outLine := func(suffix string, s device.Status) string { + r, err := cmd_device.NewGlobResult(appName+"/"+suffix, s) + if err != nil { + t.Errorf("NewGlobResult failed: %v", err) + return "" + } + return fmt.Sprintf("%v", *r) + } + var ( + runningIstc1Out = outLine("1", instanceRunning) + uninstalledIstl2Out = outLine("2", installationUninstalled) + updatingIstc3Out = outLine("3", instanceUpdating) + activeIstl4Out = outLine("4", installationActive) + notRunningIstc5Out = outLine("5", instanceNotRunning) + devc6Out = outLine("6", deviceService) + activeIstl7Out = outLine("7", installationActive) + updatingDevc8Out = outLine("8", deviceUpdating) + updatingIstc9Out = outLine("9", instanceUpdating) + ) + + noParallelismHandler := newEnforceNoParallelismHandler(t, len(allStatusResponses), []string{"app/2", "app/4", "app/7", "app/1", "app/3", "app/5", "app/9", "app/6", "app/8"}) + + for _, c := range []struct { + handler cmd_device.GlobHandler + globResponses []GlobResponse + statusResponses map[string][]interface{} + gs cmd_device.GlobSettings + globPatterns []string + expectedStdout string + expectedStderr string + expectedError string + }{ + // Verifies output is correct and in the expected order (first + // installations, then instances, then device services). + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{}, + allGlobArgs, + joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies that full parallelism runs all the handlers + // simultaneously. + { + newEnforceFullParallelismHandler(t, len(allStatusResponses)), + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{HandlerParallelism: cmd_device.FullParallelism}, + allGlobArgs, + joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies that "by kind" parallelism runs all installation + // handlers in parallel, then all instance handlers in parallel, + // then all device service handlers in parallel. + { + newEnforceKindParallelismHandler(t, 3, 4, 2), + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{HandlerParallelism: cmd_device.KindParallelism}, + allGlobArgs, + joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies that the no parallelism option runs all handlers + // sequentially. + { + noParallelismHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{HandlerParallelism: cmd_device.NoParallelism}, + allGlobArgs, + joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies "only instances" filter. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{OnlyInstances: true}, + allGlobArgs, + joinLines(runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out), + "", + "", + }, + // Verifies "only installations" filter. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{OnlyInstallations: true}, + allGlobArgs, + joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out), + "", + "", + }, + // Verifies "instance state" filter. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{InstanceStateFilter: cmd_device.InstanceStates(device.InstanceStateUpdating)}, + allGlobArgs, + joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, updatingIstc3Out, updatingIstc9Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies "instance state" filter with more than 1 state. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{InstanceStateFilter: cmd_device.InstanceStates(device.InstanceStateUpdating, device.InstanceStateRunning)}, + allGlobArgs, + joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, updatingIstc9Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies "instance state" filter with excluded state. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{InstanceStateFilter: cmd_device.ExcludeInstanceStates(device.InstanceStateUpdating)}, + allGlobArgs, + joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, notRunningIstc5Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies "instance state" filter with more than 1 excluded state. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{InstanceStateFilter: cmd_device.ExcludeInstanceStates(device.InstanceStateUpdating, device.InstanceStateRunning)}, + allGlobArgs, + joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, notRunningIstc5Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies "installation state" filter. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{InstallationStateFilter: cmd_device.InstallationStates(device.InstallationStateActive)}, + allGlobArgs, + joinLines(activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies "installation state" filter with more than 1 state. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{InstallationStateFilter: cmd_device.InstallationStates(device.InstallationStateActive, device.InstallationStateUninstalled)}, + allGlobArgs, + joinLines(uninstalledIstl2Out, activeIstl4Out, activeIstl7Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies "installation state" filter with excluded state. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{InstallationStateFilter: cmd_device.ExcludeInstallationStates(device.InstallationStateActive)}, + allGlobArgs, + joinLines(uninstalledIstl2Out, runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies "installation state" filter with more than 1 excluded state. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{InstallationStateFilter: cmd_device.ExcludeInstallationStates(device.InstallationStateActive, device.InstallationStateUninstalled)}, + allGlobArgs, + joinLines(runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies "installation state" filter + "only installations" filter. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{ + InstallationStateFilter: cmd_device.InstallationStates(device.InstallationStateActive), + OnlyInstallations: true, + }, + allGlobArgs, + joinLines(activeIstl4Out, activeIstl7Out), + "", + "", + }, + // Verifies "installation state" filter + "only instances" filter. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{ + InstallationStateFilter: cmd_device.InstallationStates(device.InstallationStateActive), + OnlyInstances: true, + }, + allGlobArgs, + joinLines(runningIstc1Out, updatingIstc3Out, notRunningIstc5Out, updatingIstc9Out), + "", + "", + }, + // Verifies "installation state" filter + "instance state" filter. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{ + InstanceStateFilter: cmd_device.InstanceStates(device.InstanceStateRunning), + InstallationStateFilter: cmd_device.InstallationStates(device.InstallationStateUninstalled), + }, + allGlobArgs, + joinLines(uninstalledIstl2Out, runningIstc1Out, devc6Out, updatingDevc8Out), + "", + "", + }, + // Verifies "only instances" filter + "only installations" filter -- no results. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{ + OnlyInstallations: true, + OnlyInstances: true, + }, + allGlobArgs, + "", + "", + "no objects found", + }, + // No glob arguments. + { + simplePrintHandler, + allGlobResponses, + allStatusResponses, + cmd_device.GlobSettings{}, + []string{}, + "", + "", + "no objects found", + }, + // No glob results. + { + simplePrintHandler, + make([]GlobResponse, 2), + allStatusResponses, + cmd_device.GlobSettings{}, + allGlobArgs, + "", + "", + "no objects found", + }, + // Error in glob. + { + simplePrintHandler, + []GlobResponse{{results: []string{"app/3", "app/4"}}, {err: fmt.Errorf("glob utter failure")}}, + allStatusResponses, + cmd_device.GlobSettings{}, + []string{"glob", "glob"}, + joinLines(activeIstl4Out, updatingIstc3Out), + fmt.Sprintf("Glob(%v) returned an error for %v: device.test:\"\".__Glob: Internal error: glob utter failure", naming.JoinAddressName(endpoint.String(), "glob"), naming.JoinAddressName(endpoint.String(), "")), + "", + }, + // Error in status. + { + simplePrintHandler, + []GlobResponse{{results: []string{"app/4", "app/3"}}, {results: []string{"app/1", "app/2"}}}, + map[string][]interface{}{ + "app/1": []interface{}{instanceRunning}, + "app/2": []interface{}{fmt.Errorf("status miserable failure")}, + "app/3": []interface{}{instanceUpdating}, + "app/4": []interface{}{installationActive}, + }, + cmd_device.GlobSettings{}, + allGlobArgs, + joinLines(activeIstl4Out, runningIstc1Out, updatingIstc3Out), + fmt.Sprintf("Status(%v) failed: device.test:<rpc.Client>\"%v\".Status: Error: status miserable failure", appName+"/2", appName+"/2"), + "", + }, + // Error in handler. + { + errOnInstallationsHandler, + []GlobResponse{{results: []string{"app/4", "app/3"}}, {results: []string{"app/1", "app/2"}}}, + map[string][]interface{}{ + "app/1": []interface{}{instanceRunning}, + "app/2": []interface{}{installationUninstalled}, + "app/3": []interface{}{instanceUpdating}, + "app/4": []interface{}{installationActive}, + }, + cmd_device.GlobSettings{}, + allGlobArgs, + joinLines(runningIstc1Out, updatingIstc3Out), + joinLines( + fmt.Sprintf("ERROR for \"%v\": handler complete failure.", appName+"/2"), + fmt.Sprintf("ERROR for \"%v\": handler complete failure.", appName+"/4")), + "encountered a total of 2 error(s)", + }, + } { + tapes.Rewind() + var rootTapeResponses []interface{} + for _, r := range c.globResponses { + rootTapeResponses = append(rootTapeResponses, r) + } + rootTape.SetResponses(rootTapeResponses...) + for n, r := range c.statusResponses { + tapes.ForSuffix(n).SetResponses(r...) + } + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + var args []string + for _, p := range c.globPatterns { + args = append(args, naming.JoinAddressName(endpoint.String(), p)) + } + err := cmd_device.Run(ctx, env, args, c.handler, c.gs) + if err != nil { + if expected, got := c.expectedError, err.Error(); expected != got { + t.Errorf("Unexpected error. Got: %v. Expected: %v.", got, expected) + } + } else if c.expectedError != "" { + t.Errorf("Expected an error (%v) but got none.", c.expectedError) + } + if expected, got := c.expectedStdout, strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected stdout. Got:\n%v\nExpected:\n%v\n", got, expected) + } + if expected, got := c.expectedStderr, strings.TrimSpace(stderr.String()); got != expected { + t.Errorf("Unexpected stderr. Got:\n%v\nExpected:\n%v\n", got, expected) + } + } +} diff --git a/x/ref/services/device/device/install.go b/x/ref/services/device/device/install.go new file mode 100644 index 000000000..b9f055f5c --- /dev/null +++ b/x/ref/services/device/device/install.go @@ -0,0 +1,83 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "encoding/json" + "fmt" + + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/services/application" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" +) + +type configFlag device.Config + +func (c *configFlag) String() string { + jsonConfig, _ := json.Marshal(c) + return string(jsonConfig) +} +func (c *configFlag) Set(s string) error { + if err := json.Unmarshal([]byte(s), c); err != nil { + return fmt.Errorf("Unmarshal(%v) failed: %v", s, err) + } + return nil +} + +var configOverride configFlag = configFlag{} + +type packagesFlag application.Packages + +func (c *packagesFlag) String() string { + jsonPackages, _ := json.Marshal(c) + return string(jsonPackages) +} +func (c *packagesFlag) Set(s string) error { + if err := json.Unmarshal([]byte(s), c); err != nil { + return fmt.Errorf("Unmarshal(%v) failed: %v", s, err) + } + return nil +} + +var packagesOverride packagesFlag = packagesFlag{} + +var cmdInstall = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runInstall), + Name: "install", + Short: "Install the given application.", + Long: "Install the given application and print the name of the new installation.", + ArgsName: "<device> <application>", + ArgsLong: ` +<device> is the vanadium object name of the device manager's app service. + +<application> is the vanadium object name of the application. +`, +} + +func init() { + cmdInstall.Flags.Var(&configOverride, "config", "JSON-encoded device.Config object, of the form: '{\"flag1\":\"value1\",\"flag2\":\"value2\"}'") + cmdInstall.Flags.Var(&packagesOverride, "packages", "JSON-encoded application.Packages object, of the form: '{\"pkg1\":{\"File\":\"object name 1\"},\"pkg2\":{\"File\":\"object name 2\"}}'") +} + +func runInstall(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 2, len(args); expected != got { + return env.UsageErrorf("install: incorrect number of arguments, expected %d, got %d", expected, got) + } + deviceName, appName := args[0], args[1] + appID, err := device.ApplicationClient(deviceName).Install(ctx, appName, device.Config(configOverride), application.Packages(packagesOverride)) + // Reset the value for any future invocations of "install" or + // "install-local" (we run more than one command per process in unit + // tests). + configOverride = configFlag{} + packagesOverride = packagesFlag{} + if err != nil { + return fmt.Errorf("Install failed: %v", err) + } + fmt.Fprintf(env.Stdout, "%s\n", naming.Join(deviceName, appID)) + return nil +} diff --git a/x/ref/services/device/device/install_test.go b/x/ref/services/device/device/install_test.go new file mode 100644 index 000000000..941b9350e --- /dev/null +++ b/x/ref/services/device/device/install_test.go @@ -0,0 +1,133 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/naming" + "v.io/v23/services/application" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/test" + + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" +) + +func TestInstallCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + + // Setup the command-line. + cmd := cmd_device.CmdRoot + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + deviceName := server.Status().Endpoints[0].Name() + appId := "myBestAppID" + cfg := device.Config{"someflag": "somevalue"} + pkg := application.Packages{"pkg": application.SignedFile{File: "somename"}} + rootTape := tapes.ForSuffix("") + for i, c := range []struct { + args []string + config device.Config + packages application.Packages + shouldErr bool + tapeResponse interface{} + expectedTape interface{} + }{ + { + []string{"blech"}, + nil, + nil, + true, + nil, + nil, + }, + { + []string{"blech1", "blech2", "blech3", "blech4"}, + nil, + nil, + true, + nil, + nil, + }, + { + []string{deviceName, appNameNoFetch, "not-valid-json"}, + nil, + nil, + true, + nil, + nil, + }, + { + []string{deviceName, appNameNoFetch}, + nil, + nil, + false, + InstallResponse{appId, nil}, + InstallStimulus{"Install", appNameNoFetch, nil, nil, application.Envelope{}, nil}, + }, + { + []string{deviceName, appNameNoFetch}, + cfg, + pkg, + false, + InstallResponse{appId, nil}, + InstallStimulus{"Install", appNameNoFetch, cfg, pkg, application.Envelope{}, nil}, + }, + } { + rootTape.SetResponses(c.tapeResponse) + if c.config != nil { + jsonConfig, err := json.Marshal(c.config) + if err != nil { + t.Fatalf("test case %d: Marshal(%v) failed: %v", i, c.config, err) + } + c.args = append([]string{fmt.Sprintf("--config=%s", string(jsonConfig))}, c.args...) + } + if c.packages != nil { + jsonPackages, err := json.Marshal(c.packages) + if err != nil { + t.Fatalf("test case %d: Marshal(%v) failed: %v", i, c.packages, err) + } + c.args = append([]string{fmt.Sprintf("--packages=%s", string(jsonPackages))}, c.args...) + } + c.args = append([]string{"install"}, c.args...) + err := v23cmd.ParseAndRunForTest(cmd, ctx, env, c.args) + if c.shouldErr { + if err == nil { + t.Fatalf("test case %d: wrongly failed to receive a non-nil error.", i) + } + if got, expected := len(rootTape.Play()), 0; got != expected { + t.Errorf("test case %d: invalid call sequence. Got %v, want %v", i, got, expected) + } + } else { + if err != nil { + t.Fatalf("test case %d: %v", i, err) + } + if expected, got := naming.Join(deviceName, appId), strings.TrimSpace(stdout.String()); got != expected { + t.Fatalf("test case %d: Unexpected output from Install. Got %q, expected %q", i, got, expected) + } + if got, expected := rootTape.Play(), []interface{}{c.expectedTape}; !reflect.DeepEqual(expected, got) { + t.Errorf("test case %d: invalid call sequence. Got %#v, want %#v", i, got, expected) + } + } + rootTape.Rewind() + stdout.Reset() + } +} diff --git a/x/ref/services/device/device/instantiate.go b/x/ref/services/device/device/instantiate.go new file mode 100644 index 000000000..aefa72433 --- /dev/null +++ b/x/ref/services/device/device/instantiate.go @@ -0,0 +1,83 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" +) + +var cmdInstantiate = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runInstantiate), + Name: "instantiate", + Short: "Create an instance of the given application.", + Long: "Create an instance of the given application, provide it with a blessing, and print the name of the new instance.", + ArgsName: "<application installation> <grant extension>", + ArgsLong: ` +<application installation> is the vanadium object name of the +application installation from which to create an instance. + +<grant extension> is used to extend the default blessing of the +current principal when blessing the app instance.`, +} + +type granter struct { + rpc.CallOpt + extension string +} + +func (g *granter) Grant(ctx *context.T, call security.Call) (security.Blessings, error) { + p := call.LocalPrincipal() + b, _ := p.BlessingStore().Default() + return p.Bless(call.RemoteBlessings().PublicKey(), b, g.extension, security.UnconstrainedUse()) +} + +func runInstantiate(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 2, len(args); expected != got { + return env.UsageErrorf("instantiate: incorrect number of arguments, expected %d, got %d", expected, got) + } + appInstallation, grant := args[0], args[1] + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + principal := v23.GetPrincipal(ctx) + + call, err := device.ApplicationClient(appInstallation).Instantiate(ctx) + if err != nil { + return fmt.Errorf("Instantiate failed: %v", err) + } + for call.RecvStream().Advance() { + switch msg := call.RecvStream().Value().(type) { + case device.BlessServerMessageInstancePublicKey: + pubKey, err := security.UnmarshalPublicKey(msg.Value) + if err != nil { + return fmt.Errorf("Instantiate failed: %v", err) + } + // TODO(caprita,rthellend): Get rid of security.UnconstrainedUse(). + toextend, _ := principal.BlessingStore().Default() + blessings, err := principal.Bless(pubKey, toextend, grant, security.UnconstrainedUse()) + if err != nil { + return fmt.Errorf("Instantiate failed: %v", err) + } + call.SendStream().Send(device.BlessClientMessageAppBlessings{Value: blessings}) + default: + fmt.Fprintf(env.Stderr, "Received unexpected message: %#v\n", msg) + } + } + var instanceID string + if instanceID, err = call.Finish(); err != nil { + return fmt.Errorf("Instantiate failed: %v", err) + } + fmt.Fprintf(env.Stdout, "%s\n", naming.Join(appInstallation, instanceID)) + return nil +} diff --git a/x/ref/services/device/device/instantiate_test.go b/x/ref/services/device/device/instantiate_test.go new file mode 100644 index 000000000..3acfcae29 --- /dev/null +++ b/x/ref/services/device/device/instantiate_test.go @@ -0,0 +1,100 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "fmt" + "reflect" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/verror" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/test" + + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" +) + +func TestInstantiateCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + + // Setup the command-line. + cmd := cmd_device.CmdRoot + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + appName := server.Status().Endpoints[0].Name() + + // Confirm that we correctly enforce the number of arguments. + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"instantiate", "nope"}); err == nil { + t.Fatalf("wrongly failed to receive a non-nil error.") + } + if expected, got := "ERROR: instantiate: incorrect number of arguments, expected 2, got 1", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from instantiate. Got %q, expected prefix %q", got, expected) + } + stdout.Reset() + stderr.Reset() + rootTape := tapes.ForSuffix("") + rootTape.Rewind() + + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"instantiate", "nope", "nope", "nope"}); err == nil { + t.Fatalf("wrongly failed to receive a non-nil error.") + } + if expected, got := "ERROR: instantiate: incorrect number of arguments, expected 2, got 3", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from instantiate. Got %q, expected prefix %q", got, expected) + } + stdout.Reset() + stderr.Reset() + rootTape.Rewind() + + // Correct operation. + rootTape.SetResponses(InstantiateResponse{ + err: nil, + instanceID: "app1", + }) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"instantiate", appName, "grant"}); err != nil { + t.Fatalf("instantiate %s %s failed: %v", appName, "grant", err) + } + + b := new(bytes.Buffer) + fmt.Fprintf(b, "%s", appName+"/app1") + if expected, got := b.String(), strings.TrimSpace(stdout.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from instantiate. Got %q, expected prefix %q", got, expected) + } + expected := []interface{}{ + "Instantiate", + } + if got := rootTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("unexpected result. Got %v want %v", got, expected) + } + rootTape.Rewind() + stdout.Reset() + stderr.Reset() + + // Error operation. + rootTape.SetResponses(InstantiateResponse{ + verror.New(errOops, nil), + "", + }) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"instantiate", appName, "grant"}); err == nil { + t.Fatalf("instantiate failed to detect error") + } + expected = []interface{}{ + "Instantiate", + } + if got := rootTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("unexpected result. Got %v want %v", got, expected) + } +} diff --git a/x/ref/services/device/device/kill.go b/x/ref/services/device/device/kill.go new file mode 100644 index 000000000..4f156df0f --- /dev/null +++ b/x/ref/services/device/device/kill.go @@ -0,0 +1,40 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + "time" + + "v.io/v23/context" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" +) + +const killDeadline = 10 * time.Second + +var cmdKill = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runKill), + Name: "kill", + Short: "Kill the given application instance.", + Long: "Kill the given application instance.", + ArgsName: "<app instance>", + ArgsLong: ` +<app instance> is the vanadium object name of the application instance to kill.`, +} + +func runKill(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 1, len(args); expected != got { + return env.UsageErrorf("kill: incorrect number of arguments, expected %d, got %d", expected, got) + } + appName := args[0] + + if err := device.ApplicationClient(appName).Kill(ctx, killDeadline); err != nil { + return fmt.Errorf("Kill failed: %v", err) + } + fmt.Fprintf(env.Stdout, "Kill succeeded\n") + return nil +} diff --git a/x/ref/services/device/device/kill_test.go b/x/ref/services/device/device/kill_test.go new file mode 100644 index 000000000..0e5d38ddc --- /dev/null +++ b/x/ref/services/device/device/kill_test.go @@ -0,0 +1,91 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "reflect" + "strings" + "testing" + "time" + + "v.io/v23" + "v.io/v23/naming" + "v.io/v23/verror" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/test" + + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" +) + +func TestKillCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + + // Setup the command-line. + cmd := cmd_device.CmdRoot + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + appName := naming.JoinAddressName(server.Status().Endpoints[0].String(), "appname") + + // Confirm that we correctly enforce the number of arguments. + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"kill"}); err == nil { + t.Fatalf("wrongly failed to receive a non-nil error.") + } + if expected, got := "ERROR: kill: incorrect number of arguments, expected 1, got 0", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from kill. Got %q, expected prefix %q", got, expected) + } + stdout.Reset() + stderr.Reset() + appTape := tapes.ForSuffix("appname") + appTape.Rewind() + + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"kill", "nope", "nope"}); err == nil { + t.Fatalf("wrongly failed to receive a non-nil error.") + } + if expected, got := "ERROR: kill: incorrect number of arguments, expected 1, got 2", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from kill. Got %q, expected prefix %q", got, expected) + } + stdout.Reset() + stderr.Reset() + appTape.Rewind() + + // Test the 'kill' command. + appTape.SetResponses(nil) + + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"kill", appName}); err != nil { + t.Fatalf("kill failed when it shouldn't: %v", err) + } + if expected, got := "Kill succeeded", strings.TrimSpace(stdout.String()); got != expected { + t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected) + } + expected := []interface{}{ + KillStimulus{"Kill", 10 * time.Second}, + } + if got := appTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("invalid call sequence. Got %v, want %v", got, expected) + } + appTape.Rewind() + stderr.Reset() + stdout.Reset() + + // Test kill with bad parameters. + appTape.SetResponses(verror.New(errOops, nil)) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"kill", appName}); err == nil { + t.Fatalf("wrongly didn't receive a non-nil error.") + } + // expected the same. + if got := appTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("invalid call sequence. Got %v, want %v", got, expected) + } +} diff --git a/x/ref/services/device/device/local_install.go b/x/ref/services/device/device/local_install.go new file mode 100644 index 000000000..c444df4f3 --- /dev/null +++ b/x/ref/services/device/device/local_install.go @@ -0,0 +1,306 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/services/binary" + "v.io/v23/services/device" + "v.io/v23/services/repository" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/services/internal/packages" +) + +var cmdInstallLocal = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runInstallLocal), + Name: "install-local", + Short: "Install the given application from the local system.", + Long: "Install the given application specified using a local path, and print the name of the new installation.", + ArgsName: "<device> <title> [ENV=VAL ...] binary [--flag=val ...] [PACKAGES path ...]", + ArgsLong: ` +<device> is the vanadium object name of the device manager's app service. + +<title> is the app title. + +This is followed by an arbitrary number of environment variable settings, the +local path for the binary to install, and arbitrary flag settings and args. +Optionally, this can be followed by 'PACKAGES' and a list of local files and +directories to be installed as packages for the app`} + +func init() { + cmdInstallLocal.Flags.Var(&configOverride, "config", "JSON-encoded device.Config object, of the form: '{\"flag1\":\"value1\",\"flag2\":\"value2\"}'") + cmdInstallLocal.Flags.Var(&packagesOverride, "packages", "JSON-encoded application.Packages object, of the form: '{\"pkg1\":{\"File\":\"local file path1\"},\"pkg2\":{\"File\":\"local file path 2\"}}'") +} + +type mapDispatcher map[string]interface{} + +func (d mapDispatcher) Lookup(_ *context.T, suffix string) (interface{}, security.Authorizer, error) { + o, ok := d[suffix] + if !ok { + return nil, nil, fmt.Errorf("suffix %s not found", suffix) + } + // TODO(caprita): Do not allow everyone, even for a short-lived server. + return o, security.AllowEveryone(), nil +} + +type mapServer struct { + name string + dispatcher mapDispatcher +} + +func (ms *mapServer) serve(name string, object interface{}) (string, error) { + if _, ok := ms.dispatcher[name]; ok { + return "", fmt.Errorf("can't have more than one object with name %v", name) + } + ms.dispatcher[name] = object + return naming.Join(ms.name, name), nil +} + +func createServer(ctx *context.T, stderr io.Writer) (*context.T, *mapServer, func(), error) { + dispatcher := make(mapDispatcher) + + ctx = v23.WithListenSpec(ctx, rpc.ListenSpec{}) + ctx, cancel := context.WithCancel(ctx) + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", dispatcher) + if err != nil { + return nil, nil, nil, err + } + name := server.Status().Endpoints[0].Name() + cleanup := func() { + cancel() + <-server.Closed() + } + return ctx, &mapServer{name: name, dispatcher: dispatcher}, cleanup, nil +} + +var errNotImplemented = fmt.Errorf("method not implemented") + +type binaryInvoker string + +func (binaryInvoker) Create(*context.T, rpc.ServerCall, int32, repository.MediaInfo) error { + return errNotImplemented +} + +func (binaryInvoker) Delete(*context.T, rpc.ServerCall) error { + return errNotImplemented +} + +func (i binaryInvoker) Download(ctx *context.T, call repository.BinaryDownloadServerCall, _ int32) error { + fileName := string(i) + fStat, err := os.Stat(fileName) + if err != nil { + return err + } + ctx.VI(1).Infof("Download commenced for %v (%v bytes)", fileName, fStat.Size()) + file, err := os.Open(fileName) + if err != nil { + return err + } + defer file.Close() + bufferLength := 4096 + buffer := make([]byte, bufferLength) + sender := call.SendStream() + var sentTotal int64 + const logChunk = 1 << 20 + for { + n, err := file.Read(buffer) + switch err { + case io.EOF: + ctx.VI(1).Infof("Download complete for %v (%v bytes)", fileName, fStat.Size()) + return nil + case nil: + if err := sender.Send(buffer[:n]); err != nil { + return err + } + if sentTotal/logChunk < (sentTotal+int64(n))/logChunk { + ctx.VI(1).Infof("Download progress for %v: %v/%v", fileName, sentTotal+int64(n), fStat.Size()) + } + sentTotal += int64(n) + default: + return err + } + } +} + +func (binaryInvoker) DownloadUrl(*context.T, rpc.ServerCall) (string, int64, error) { + return "", 0, errNotImplemented +} + +func (i binaryInvoker) Stat(*context.T, rpc.ServerCall) ([]binary.PartInfo, repository.MediaInfo, error) { + fileName := string(i) + h := md5.New() + bytes, err := ioutil.ReadFile(fileName) + if err != nil { + return []binary.PartInfo{}, repository.MediaInfo{}, err + } + h.Write(bytes) + part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))} + return []binary.PartInfo{part}, packages.MediaInfoForFileName(fileName), nil +} + +func (binaryInvoker) Upload(*context.T, repository.BinaryUploadServerCall, int32) error { + return errNotImplemented +} + +func (binaryInvoker) GetPermissions(*context.T, rpc.ServerCall) (perms access.Permissions, version string, err error) { + return nil, "", errNotImplemented +} + +func (binaryInvoker) SetPermissions(_ *context.T, _ rpc.ServerCall, perms access.Permissions, version string) error { + return errNotImplemented +} + +type envelopeInvoker application.Envelope + +func (i envelopeInvoker) Match(*context.T, rpc.ServerCall, []string) (application.Envelope, error) { + return application.Envelope(i), nil +} + +func (envelopeInvoker) GetPermissions(*context.T, rpc.ServerCall) (perms access.Permissions, version string, err error) { + return nil, "", errNotImplemented +} + +func (envelopeInvoker) SetPermissions(*context.T, rpc.ServerCall, access.Permissions, string) error { + return errNotImplemented +} + +func (envelopeInvoker) TidyNow(*context.T, rpc.ServerCall) error { + return errNotImplemented +} + +func servePackage(p string, ms *mapServer, tmpZipDir string) (string, string, error) { + info, err := os.Stat(p) + if os.IsNotExist(err) { + return "", "", fmt.Errorf("%v not found: %v", p, err) + } else if err != nil { + return "", "", fmt.Errorf("Stat(%v) failed: %v", p, err) + } + pkgName := naming.Join("packages", info.Name()) + fileName := p + // Directory packages first get zip'ped. + if info.IsDir() { + fileName = filepath.Join(tmpZipDir, info.Name()+".zip") + if err := packages.CreateZip(fileName, p); err != nil { + return "", "", err + } + } + name, err := ms.serve(pkgName, repository.BinaryServer(binaryInvoker(fileName))) + return info.Name(), name, err +} + +// runInstallLocal creates a new envelope on the fly from the provided +// arguments, and then points the device manager back to itself for downloading +// the app envelope and binary. +// +// It sets up an app and binary server that only lives for the duration of the +// command, and listens on the profile's listen spec. +func runInstallLocal(ctx *context.T, env *cmdline.Env, args []string) error { + if expectedMin, got := 2, len(args); got < expectedMin { + return env.UsageErrorf("install-local: incorrect number of arguments, expected at least %d, got %d", expectedMin, got) + } + deviceName, title := args[0], args[1] + args = args[2:] + envelope := application.Envelope{Title: title} + // Extract the environment settings, binary, and arguments. + firstNonEnv := len(args) + for i, arg := range args { + if strings.Index(arg, "=") <= 0 { + firstNonEnv = i + break + } + } + envelope.Env = args[:firstNonEnv] + args = args[firstNonEnv:] + if len(args) == 0 { + return env.UsageErrorf("install-local: missing binary") + } + binary := args[0] + args = args[1:] + firstNonArg, firstPackage := len(args), len(args) + for i, arg := range args { + if arg == "PACKAGES" { + firstNonArg = i + firstPackage = i + 1 + break + } + } + envelope.Args = args[:firstNonArg] + pkgs := args[firstPackage:] + if _, err := os.Stat(binary); err != nil { + return fmt.Errorf("binary %v not found: %v", binary, err) + } + ctx, server, cancel, err := createServer(ctx, env.Stderr) + if err != nil { + return fmt.Errorf("failed to create server: %v", err) + } + defer cancel() + envelope.Binary.File, err = server.serve("binary", repository.BinaryServer(binaryInvoker(binary))) + if err != nil { + return err + } + ctx.VI(1).Infof("binary %v serving as %v", binary, envelope.Binary.File) + + // For each package dir/file specified in the arguments list, set up an + // object in the binary service to serve that package, and add the + // object name to the envelope's Packages map. + tmpZipDir, err := ioutil.TempDir("", "packages") + if err != nil { + return fmt.Errorf("failed to create a temp dir for zip packages: %v", err) + } + defer os.RemoveAll(tmpZipDir) + for _, p := range pkgs { + if envelope.Packages == nil { + envelope.Packages = make(application.Packages) + } + pname, oname, err := servePackage(p, server, tmpZipDir) + if err != nil { + return err + } + ctx.VI(1).Infof("package %v serving as %v", pname, oname) + envelope.Packages[pname] = application.SignedFile{File: oname} + } + packagesRewritten := application.Packages{} + for pname, pspec := range packagesOverride { + _, oname, err := servePackage(pspec.File, server, tmpZipDir) + if err != nil { + return err + } + ctx.VI(1).Infof("package %v serving as %v", pname, oname) + pspec.File = oname + packagesRewritten[pname] = pspec + } + appName, err := server.serve("application", repository.ApplicationServer(envelopeInvoker(envelope))) + if err != nil { + return err + } + ctx.VI(1).Infof("application serving envelope as %v", appName) + appID, err := device.ApplicationClient(deviceName).Install(ctx, appName, device.Config(configOverride), packagesRewritten) + // Reset the value for any future invocations of "install" or + // "install-local" (we run more than one command per process in unit + // tests). + configOverride = configFlag{} + packagesOverride = packagesFlag{} + if err != nil { + return fmt.Errorf("Install failed: %v", err) + } + fmt.Fprintf(env.Stdout, "%s\n", naming.Join(deviceName, appID)) + return nil +} diff --git a/x/ref/services/device/device/local_install_test.go b/x/ref/services/device/device/local_install_test.go new file mode 100644 index 000000000..4fb34441d --- /dev/null +++ b/x/ref/services/device/device/local_install_test.go @@ -0,0 +1,257 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/naming" + "v.io/v23/security" + "v.io/v23/services/application" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/test" + + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" +) + +func createFile(t *testing.T, path string, contents string) { + if err := ioutil.WriteFile(path, []byte(contents), 0700); err != nil { + t.Fatalf("Failed to create %v: %v", path, err) + } +} + +func TestInstallLocalCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + // Setup the command-line. + cmd := cmd_device.CmdRoot + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + deviceName := server.Status().Endpoints[0].Name() + const appTitle = "Appo di tutti Appi" + binary := os.Args[0] + fi, err := os.Stat(binary) + if err != nil { + t.Fatalf("Failed to stat %v: %v", binary, err) + } + binarySize := fi.Size() + rootTape := tapes.ForSuffix("") + for i, c := range []struct { + args []string + stderrSubstr string + }{ + { + []string{deviceName}, "incorrect number of arguments", + }, + { + []string{deviceName, appTitle}, "missing binary", + }, + { + []string{deviceName, appTitle, "a=b"}, "missing binary", + }, + { + []string{deviceName, appTitle, "foo"}, "binary foo not found", + }, + { + []string{deviceName, appTitle, binary, "PACKAGES", "foo"}, "foo not found", + }, + } { + c.args = append([]string{"install-local"}, c.args...) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, c.args); err == nil { + t.Fatalf("test case %d: wrongly failed to receive a non-nil error.", i) + } else { + fmt.Fprintln(&stderr, "ERROR:", err) + if want, got := c.stderrSubstr, stderr.String(); !strings.Contains(got, want) { + t.Errorf("test case %d: %q not found in stderr: %q", i, want, got) + } + } + if got, expected := len(rootTape.Play()), 0; got != expected { + t.Errorf("test case %d: invalid call sequence. Got %v, want %v", i, got, expected) + } + rootTape.Rewind() + stdout.Reset() + stderr.Reset() + } + emptySig := security.Signature{} + emptyBlessings := security.Blessings{} + cfg := device.Config{"someflag": "somevalue"} + + testPackagesDir, err := ioutil.TempDir("", "testdir") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(testPackagesDir) + pkgFile1 := filepath.Join(testPackagesDir, "file1.txt") + createFile(t, pkgFile1, "1234567") + pkgFile2 := filepath.Join(testPackagesDir, "file2") + createFile(t, pkgFile2, string([]byte{0x01, 0x02, 0x03, 0x04})) + pkgDir1 := filepath.Join(testPackagesDir, "dir1") + if err := os.Mkdir(pkgDir1, 0700); err != nil { + t.Fatalf("Failed to create dir1: %v", err) + } + createFile(t, filepath.Join(pkgDir1, "f1"), "123") + createFile(t, filepath.Join(pkgDir1, "f2"), "456") + createFile(t, filepath.Join(pkgDir1, "f3"), "7890") + + pkgFile3 := filepath.Join(testPackagesDir, "file3") + createFile(t, pkgFile3, "12345") + pkgFile4 := filepath.Join(testPackagesDir, "file4") + createFile(t, pkgFile4, "123") + pkgDir2 := filepath.Join(testPackagesDir, "dir2") + if err := os.Mkdir(pkgDir2, 0700); err != nil { + t.Fatalf("Failed to create dir2: %v", err) + } + createFile(t, filepath.Join(pkgDir2, "f1"), "123456") + createFile(t, filepath.Join(pkgDir2, "f2"), "78") + pkg := application.Packages{ + "overridepkg1": application.SignedFile{File: pkgFile3}, + "overridepkg2": application.SignedFile{File: pkgFile4}, + "overridepkg3": application.SignedFile{File: pkgDir2}, + } + + for i, c := range []struct { + args []string + config device.Config + packages application.Packages + expectedTape interface{} + }{ + { + []string{deviceName, appTitle, binary}, + nil, + nil, + InstallStimulus{ + "Install", + appNameAfterFetch, + nil, + nil, + application.Envelope{ + Title: appTitle, + Binary: application.SignedFile{ + File: binaryNameAfterFetch, + Signature: emptySig, + }, + Publisher: emptyBlessings, + }, + map[string]int64{"binary": binarySize}}, + }, + { + []string{deviceName, appTitle, binary}, + cfg, + nil, + InstallStimulus{ + "Install", + appNameAfterFetch, + cfg, + nil, + application.Envelope{ + Title: appTitle, + Binary: application.SignedFile{ + File: binaryNameAfterFetch, + Signature: emptySig, + }, + Publisher: emptyBlessings, + }, + map[string]int64{"binary": binarySize}}, + }, + { + []string{deviceName, appTitle, "ENV1=V1", "ENV2=V2", binary, "FLAG1=V1", "FLAG2=V2"}, + nil, + nil, + InstallStimulus{ + "Install", + appNameAfterFetch, + nil, + nil, + application.Envelope{ + Title: appTitle, + Binary: application.SignedFile{ + File: binaryNameAfterFetch, + Signature: emptySig, + }, + Publisher: emptyBlessings, + Env: []string{"ENV1=V1", "ENV2=V2"}, + Args: []string{"FLAG1=V1", "FLAG2=V2"}, + }, + map[string]int64{"binary": binarySize}}, + }, + { + []string{deviceName, appTitle, "ENV=V", binary, "FLAG=V", "PACKAGES", pkgFile1, pkgFile2, pkgDir1}, + nil, + pkg, + InstallStimulus{"Install", + appNameAfterFetch, + nil, + nil, + application.Envelope{ + Title: appTitle, + Binary: application.SignedFile{ + File: binaryNameAfterFetch, + Signature: emptySig, + }, + Publisher: emptyBlessings, + Env: []string{"ENV=V"}, + Args: []string{"FLAG=V"}, + }, + map[string]int64{ + "binary": binarySize, + "packages/file1.txt": 7, + "packages/file2": 4, + "packages/dir1": 10, + "overridepackages/overridepkg1": 5, + "overridepackages/overridepkg2": 3, + "overridepackages/overridepkg3": 8, + }, + }, + }, + } { + const appId = "myBestAppID" + rootTape.SetResponses(InstallResponse{appId, nil}) + if c.config != nil { + jsonConfig, err := json.Marshal(c.config) + if err != nil { + t.Fatalf("test case %d: Marshal(%v) failed: %v", i, c.config, err) + } + c.args = append([]string{fmt.Sprintf("--config=%s", string(jsonConfig))}, c.args...) + } + if c.packages != nil { + jsonPackages, err := json.Marshal(c.packages) + if err != nil { + t.Fatalf("test case %d: Marshal(%v) failed: %v", i, c.packages, err) + } + c.args = append([]string{fmt.Sprintf("--packages=%s", string(jsonPackages))}, c.args...) + } + c.args = append([]string{"install-local"}, c.args...) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, c.args); err != nil { + t.Fatalf("test case %d: %v", i, err) + } + if expected, got := naming.Join(deviceName, appId), strings.TrimSpace(stdout.String()); got != expected { + t.Fatalf("test case %d: Unexpected output from Install. Got %q, expected %q", i, got, expected) + } + if got, expected := rootTape.Play(), []interface{}{c.expectedTape}; !reflect.DeepEqual(expected, got) { + t.Errorf("test case %d: Invalid call sequence. Got %#v, want %#v", i, got, expected) + } + rootTape.Rewind() + stdout.Reset() + stderr.Reset() + } +} diff --git a/x/ref/services/device/device/ls.go b/x/ref/services/device/device/ls.go new file mode 100644 index 000000000..648710565 --- /dev/null +++ b/x/ref/services/device/device/ls.go @@ -0,0 +1,32 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + "io" + + "v.io/v23/context" + + "v.io/x/lib/cmdline" +) + +var cmdLs = &cmdline.Command{ + Name: "ls", + Short: "List applications.", + Long: "List application installations or instances.", + ArgsName: "<app name patterns...>", + ArgsLong: ` +<app name patterns...> are vanadium object names or glob name patterns corresponding to application installations and instances.`, +} + +func init() { + globify(cmdLs, runLs, new(GlobSettings)) +} + +func runLs(entry GlobResult, _ *context.T, stdout, _ io.Writer) error { + fmt.Fprintf(stdout, "%v\n", entry.Name) + return nil +} diff --git a/x/ref/services/device/device/ls_test.go b/x/ref/services/device/device/ls_test.go new file mode 100644 index 000000000..5a74820fd --- /dev/null +++ b/x/ref/services/device/device/ls_test.go @@ -0,0 +1,160 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/naming" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/test" + + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" +) + +// TestLsCommand verifies the device ls command. It also acts as a test for the +// glob functionality, by trying out various combinations of +// instances/installations in glob results. +func TestLsCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + cmd := cmd_device.CmdRoot + endpoint := server.Status().Endpoints[0] + appName := naming.JoinAddressName(endpoint.String(), "app") + rootTape := tapes.ForSuffix("") + cannedGlobResponses := [][]string{ + []string{"app/3", "app/4", "app/6", "app/5"}, + []string{"app/2", "app/1"}, + } + cannedStatusResponses := map[string][]interface{}{ + "app/1": []interface{}{instanceRunning}, + "app/2": []interface{}{installationUninstalled}, + "app/3": []interface{}{instanceUpdating}, + "app/4": []interface{}{installationActive}, + "app/5": []interface{}{instanceNotRunning}, + "app/6": []interface{}{installationActive}, + } + for _, c := range []struct { + globResponses [][]string + statusResponses map[string][]interface{} + lsFlags []string + globPatterns []string + expected string + }{ + { + cannedGlobResponses, + cannedStatusResponses, + []string{}, + []string{"glob1", "glob2"}, + joinLines(appName+"/2", appName+"/4", appName+"/6", appName+"/1", appName+"/3", appName+"/5"), + }, + { + cannedGlobResponses, + cannedStatusResponses, + []string{"--only-instances"}, + []string{"glob1", "glob2"}, + joinLines(appName+"/1", appName+"/3", appName+"/5"), + }, + { + cannedGlobResponses, + cannedStatusResponses, + []string{"--only-installations"}, + []string{"glob1", "glob2"}, + joinLines(appName+"/2", appName+"/4", appName+"/6"), + }, + { + cannedGlobResponses, + cannedStatusResponses, + []string{"--instance-state=Running,Updating"}, + []string{"glob1", "glob2"}, + joinLines(appName+"/2", appName+"/4", appName+"/6", appName+"/1", appName+"/3"), + }, + { + cannedGlobResponses, + cannedStatusResponses, + []string{"--installation-state=Active"}, + []string{"glob1", "glob2"}, + joinLines(appName+"/4", appName+"/6", appName+"/1", appName+"/3", appName+"/5"), + }, + { + cannedGlobResponses, + cannedStatusResponses, + []string{"--only-installations", "--installation-state=Active"}, + []string{"glob1", "glob2"}, + joinLines(appName+"/4", appName+"/6"), + }, + { + cannedGlobResponses, + cannedStatusResponses, + []string{"--only-instances", "--installation-state=Active"}, + []string{"glob1", "glob2"}, + joinLines(appName+"/1", appName+"/3", appName+"/5"), + }, + { + [][]string{ + []string{"app/1", "app/2"}, + []string{"app/2", "app/3"}, + []string{"app/2", "app/3"}, + }, + map[string][]interface{}{ + "app/1": []interface{}{instanceRunning}, + "app/2": []interface{}{installationUninstalled, installationUninstalled}, + "app/3": []interface{}{instanceUpdating}, + }, + []string{}, + []string{"glob1", "glob2"}, + joinLines(appName+"/2", appName+"/2", appName+"/1", appName+"/3"), + }, + { + [][]string{ + []string{"app/1", "app/2"}, + []string{"app/2", "app/3"}, + []string{"app/2", "app/3"}, + }, + map[string][]interface{}{ + "app/1": []interface{}{instanceRunning}, + "app/2": []interface{}{installationUninstalled, installationUninstalled, installationUninstalled}, + "app/3": []interface{}{instanceUpdating, instanceUpdating}, + }, + []string{"--only-installations"}, + []string{"glob1", "glob2", "glob3"}, + joinLines(appName+"/2", appName+"/2", appName+"/2"), + }, + } { + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + tapes.Rewind() + var rootTapeResponses []interface{} + for _, r := range c.globResponses { + rootTapeResponses = append(rootTapeResponses, GlobResponse{results: r}) + } + rootTape.SetResponses(rootTapeResponses...) + for n, r := range c.statusResponses { + tapes.ForSuffix(n).SetResponses(r...) + } + args := append([]string{"ls"}, c.lsFlags...) + for _, p := range c.globPatterns { + args = append(args, naming.JoinAddressName(endpoint.String(), p)) + } + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, args); err != nil { + t.Errorf("%v", err) + } + + if expected, got := c.expected, strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected output from ls. Got %q, expected %q", got, expected) + } + cmd_device.ResetGlobSettings() + } +} diff --git a/x/ref/services/device/device/publish.go b/x/ref/services/device/device/publish.go new file mode 100644 index 000000000..0812212c9 --- /dev/null +++ b/x/ref/services/device/device/publish.go @@ -0,0 +1,261 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + "time" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/services/permissions" + "v.io/v23/verror" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/services/internal/binarylib" + "v.io/x/ref/services/repository" +) + +// TODO(caprita): Add unit test. + +// TODO(caprita): Extend to include env, args, packages. + +var cmdPublish = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runPublish), + Name: "publish", + Short: "Publish the given application(s).", + Long: ` +Publishes the given application(s) to the binary and application servers. +The binaries should be in $JIRI_ROOT/release/go/bin/[<GOOS>_<GOARCH>] by default (can be overrriden with --from). +By default the binary name is used as the name of the application envelope, and as the +title in the envelope. However, <envelope-name> and <title> can be specified explicitly +using :<envelope-name> and @<title>. +The binary is published as <binserv>/<binary name>/<GOOS>-<GOARCH>/<TIMESTAMP>. +The application envelope is published as <appserv>/<envelope-name>/<TIMESTAMP>. +Optionally, adds blessing patterns to the Read and Resolve AccessLists.`, + ArgsName: "<binary name>[:<envelope-name>][@<title>] ...", +} + +var binaryService, applicationService, readBlessings, goarchFlag, goosFlag, fromFlag string +var addPublisher bool +var minValidPublisherDuration time.Duration + +func init() { + cmdPublish.Flags.StringVar(&binaryService, "binserv", "binaries", "Name of binary service.") + cmdPublish.Flags.StringVar(&applicationService, "appserv", "applications", "Name of application service.") + cmdPublish.Flags.StringVar(&goosFlag, "goos", runtime.GOOS, "GOOS for application. The default is the value of runtime.GOOS.") + cmdPublish.Flags.Lookup("goos").DefValue = "<runtime.GOOS>" + cmdPublish.Flags.StringVar(&goarchFlag, "goarch", runtime.GOARCH, "GOARCH for application. The default is the value of runtime.GOARCH.") + cmdPublish.Flags.Lookup("goarch").DefValue = "<runtime.GOARCH>" + cmdPublish.Flags.StringVar(&readBlessings, "readers", "dev.v.io", "If non-empty, comma-separated blessing patterns to add to Read and Resolve AccessList.") + cmdPublish.Flags.BoolVar(&addPublisher, "add-publisher", true, "If true, add a publisher blessing to the application envelope") + cmdPublish.Flags.DurationVar(&minValidPublisherDuration, "publisher-min-validity", 30*time.Hour, "Publisher blessings that are valid for less than this amount of time are considered invalid") + cmdPublish.Flags.StringVar(&fromFlag, "from", "", "Location of binaries to be published. Defaults to $JIRI_ROOT/release/go/bin/[<GOOS>_<GOARCH>]") +} + +func setAccessLists(ctx *context.T, env *cmdline.Env, von string) error { + if readBlessings == "" { + return nil + } + perms, version, err := permissions.ObjectClient(von).GetPermissions(ctx) + if err != nil { + // TODO(caprita): This is a workaround until we sort out the + // default AccessLists for applicationd (see issue #1317). At that + // time, uncomment the line below. + // + // return err + perms = make(access.Permissions) + } + for _, blessing := range strings.Split(readBlessings, ",") { + for _, tag := range []access.Tag{access.Read, access.Resolve} { + perms.Add(security.BlessingPattern(blessing), string(tag)) + } + } + if err := permissions.ObjectClient(von).SetPermissions(ctx, perms, version); err != nil { + return err + } + fmt.Fprintf(env.Stdout, "Added patterns %q to Read,Resolve AccessList for %q\n", readBlessings, von) + return nil +} + +func publishOne(ctx *context.T, env *cmdline.Env, binPath, binary string) error { + binaryName, envelopeName, title := binary, binary, binary + binaryRE := regexp.MustCompile(`^([^:@]+)(:[^@]+)?(@.+)?$`) + if parts := binaryRE.FindStringSubmatch(binary); len(parts) == 4 { + binaryName = parts[1] + envelopeName, title = binaryName, binaryName + if len(parts[2]) > 1 { + envelopeName = parts[2][1:] + } + if len(parts[3]) > 1 { + title = parts[3][1:] + } + } else { + return fmt.Errorf("invalid binary spec (%v)", binary) + } + + // Step 1, upload the binary to the binary service. + + // TODO(caprita): Instead of the current timestamp, use each binary's + // BuildTimestamp from the buildinfo. + timestamp := time.Now().UTC().Format(time.RFC3339) + binaryVON := naming.Join(binaryService, binaryName, fmt.Sprintf("%s-%s", goosFlag, goarchFlag), timestamp) + binaryFile := filepath.Join(binPath, binaryName) + var binarySig *security.Signature + var err error + for i := 0; ; i++ { + binarySig, err = binarylib.UploadFromFile(ctx, binaryVON, binaryFile) + if verror.ErrorID(err) == verror.ErrExist.ID { + newTS := fmt.Sprintf("%s-%d", timestamp, i+1) + binaryVON = naming.Join(binaryService, binaryName, fmt.Sprintf("%s-%s", goosFlag, goarchFlag), newTS) + continue + } + if err == nil { + break + } + return err + } + fmt.Fprintf(env.Stdout, "Binary %q uploaded from file %s\n", binaryVON, binaryFile) + + // Step 2, set the perms for the uploaded binary. + + if err := setAccessLists(ctx, env, binaryVON); err != nil { + return err + } + + // Step 3, download existing envelope (or create a new one), update, and + // upload to application service. + + // TODO(caprita): use the profile detection machinery and/or let user + // specify the profile by hand. + profile := fmt.Sprintf("%s-%s", goosFlag, goarchFlag) + appVON := naming.Join(applicationService, envelopeName) + appClient := repository.ApplicationClient(appVON) + envelope, err := appClient.Match(ctx, []string{profile}) + // TODO(caprita): Fix https://github.com/vanadium/issues/issues/679 + if errID := verror.ErrorID(err); errID == verror.ErrNoExist.ID || errID == "v.io/x/ref/services/application/applicationd.InvalidSuffix" { + // There was nothing published yet, create a new envelope. + envelope = application.Envelope{Title: title} + } else if err != nil { + return err + } else { + // We are going to be updating an existing envelope + + // Complain if a title was specified explicitly and does not match the one in the + // envelope, because we are not going to update the one in the envelope + if title != binaryName && title != envelope.Title { + return fmt.Errorf("Specified title (%v) does not match title in existing envelope (%v)", title, envelope.Title) + } + } + + envelope.Binary.File = binaryVON + if addPublisher { + publisher, err := getPublisherBlessing(ctx, strings.Join([]string{"apps", "published", title}, security.ChainSeparator)) + if err != nil { + return err + } + envelope.Publisher = publisher + envelope.Binary.Signature = *binarySig + } else { + // We must explicitly clear these fields because we might be trying to update + // an envelope that previously pointed at a signed binary. + envelope.Binary.Signature = security.Signature{} + envelope.Publisher = security.Blessings{} + } + appVON = naming.Join(appVON, timestamp) + appClient = repository.ApplicationClient(appVON) + if err := appClient.Put(ctx, profile, envelope, false); err != nil { + // NOTE(caprita): We don't retry if an envelope already exists + // at the versioned name, as we do when uploading binaries. In + // the case of binaries, it's likely that the same binary is + // uploaded more than once in a given second, due to apps + // sharing the same binary. The scenarios where the same app is + // published repeatedly in a short time-frame are expected to be + // rare, and the operator can retry manually in such cases. + return err + } + fmt.Fprintf(env.Stdout, "Published %q\n", appVON) + + // Step 4, set the perms for the uploaded envelope. + + if err := setAccessLists(ctx, env, appVON); err != nil { + return err + } + return nil +} + +func runPublish(ctx *context.T, env *cmdline.Env, args []string) error { + if expectedMin, got := 1, len(args); got < expectedMin { + return env.UsageErrorf("publish: incorrect number of arguments, expected at least %d, got %d", expectedMin, got) + } + binaries := args + binPath := fromFlag + if binPath == "" { + vroot := env.Vars["JIRI_ROOT"] + if vroot == "" { + return env.UsageErrorf("publish: $JIRI_ROOT environment variable should be set") + } + binPath = filepath.Join(vroot, "release/go/bin") + if goosFlag != runtime.GOOS || goarchFlag != runtime.GOARCH { + binPath = filepath.Join(binPath, fmt.Sprintf("%s_%s", goosFlag, goarchFlag)) + } + } + if fi, err := os.Stat(binPath); err != nil { + return env.UsageErrorf("publish: failed to stat %v: %v", binPath, err) + } else if !fi.IsDir() { + return env.UsageErrorf("publish: %v is not a directory", binPath) + } + if binaryService == "" { + return env.UsageErrorf("publish: --binserv must point to a binary service name") + } + if applicationService == "" { + return env.UsageErrorf("publish: --appserv must point to an application service name") + } + var lastErr error + for _, b := range binaries { + if err := publishOne(ctx, env, binPath, b); err != nil { + fmt.Fprintf(env.Stderr, "Failed to publish %q: %v\n", b, err) + lastErr = err + } + } + return lastErr +} + +func getPublisherBlessing(ctx *context.T, extension string) (security.Blessings, error) { + p := v23.GetPrincipal(ctx) + bDef, _ := p.BlessingStore().Default() + b, err := p.Bless(p.PublicKey(), bDef, extension, security.UnconstrainedUse()) + if err != nil { + return security.Blessings{}, err + } + + // We need to make sure that the blessing is usable as a publisher blessing -- in + // practice this current means that it has no caveats other than expiration. We + // test this by putting it into a call object and verifying that the blessing will + // not be rejected + call := security.NewCall(&security.CallParams{ + RemoteBlessings: b, + LocalBlessings: bDef, + LocalPrincipal: p, + Timestamp: time.Now().Add(minValidPublisherDuration), + }) + accepted, rejected := security.RemoteBlessingNames(ctx, call) + if len(accepted) == 0 { + return security.Blessings{}, fmt.Errorf("All blessings are invalid: %v", rejected) + } + if len(rejected) > 0 { + fmt.Fprintf(os.Stderr, "Warning: Some invalid blessings are present: %v", rejected) + } + return b, nil +} diff --git a/x/ref/services/device/device/root.go b/x/ref/services/device/device/root.go new file mode 100644 index 000000000..48f6fb0c9 --- /dev/null +++ b/x/ref/services/device/device/root.go @@ -0,0 +1,29 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The following enables go generate to generate the doc.go file. +//go:generate gendoc . + +package main + +import ( + "regexp" + + "v.io/x/lib/cmdline" + _ "v.io/x/ref/runtime/factories/roaming" +) + +var CmdRoot = &cmdline.Command{ + Name: "device", + Short: "facilitates interaction with the Vanadium device manager", + Long: ` +Command device facilitates interaction with the Vanadium device manager. +`, + Children: []*cmdline.Command{cmdInstall, cmdInstallLocal, cmdUninstall, cmdAssociate, cmdDescribe, cmdClaim, cmdInstantiate, cmdDelete, cmdRun, cmdKill, cmdRevert, cmdUpdate, cmdStatus, cmdDebug, cmdACL, cmdPublish, cmdLs}, +} + +func main() { + cmdline.HideGlobalFlagsExcept(regexp.MustCompile(`^((v23\.namespace\.root)|(v23\.proxy))$`)) + cmdline.Main(CmdRoot) +} diff --git a/x/ref/services/device/device/run.go b/x/ref/services/device/device/run.go new file mode 100644 index 000000000..652870896 --- /dev/null +++ b/x/ref/services/device/device/run.go @@ -0,0 +1,37 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + + "v.io/v23/context" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" +) + +var cmdRun = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runRun), + Name: "run", + Short: "Run the given application instance.", + Long: "Run the given application instance.", + ArgsName: "<app instance>", + ArgsLong: ` +<app instance> is the vanadium object name of the application instance to run.`, +} + +func runRun(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 1, len(args); expected != got { + return env.UsageErrorf("run: incorrect number of arguments, expected %d, got %d", expected, got) + } + appName := args[0] + + if err := device.ApplicationClient(appName).Run(ctx); err != nil { + return fmt.Errorf("Run failed: %v,\nView log with:\n debug logs read `debug glob %s/logs/STDERR-*`", err, appName) + } + fmt.Fprintf(env.Stdout, "Run succeeded\n") + return nil +} diff --git a/x/ref/services/device/device/run_test.go b/x/ref/services/device/device/run_test.go new file mode 100644 index 000000000..7d9f5d90f --- /dev/null +++ b/x/ref/services/device/device/run_test.go @@ -0,0 +1,11 @@ +// Copyright 2015 The Vanadium 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 main_test + +import "testing" + +func TestRunCommand(t *testing.T) { + testHelper(t, "run", "Run") +} diff --git a/x/ref/services/device/device/status.go b/x/ref/services/device/device/status.go new file mode 100644 index 000000000..bb576c959 --- /dev/null +++ b/x/ref/services/device/device/status.go @@ -0,0 +1,41 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + "io" + + "v.io/v23/context" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" +) + +var cmdStatus = &cmdline.Command{ + Name: "status", + Short: "Get device manager or application status.", + Long: "Get the status of the device manager or application instances and installations.", + ArgsName: "<name patterns...>", + ArgsLong: ` +<name patterns...> are vanadium object names or glob name patterns corresponding to the device manager service, or to application installations and instances.`, +} + +func init() { + globify(cmdStatus, runStatus, new(GlobSettings)) +} + +func runStatus(entry GlobResult, _ *context.T, stdout, _ io.Writer) error { + switch s := entry.Status.(type) { + case device.StatusInstance: + fmt.Fprintf(stdout, "Instance %v [State:%v,Version:%v]\n", entry.Name, s.Value.State, s.Value.Version) + case device.StatusInstallation: + fmt.Fprintf(stdout, "Installation %v [State:%v,Version:%v]\n", entry.Name, s.Value.State, s.Value.Version) + case device.StatusDevice: + fmt.Fprintf(stdout, "Device Service %v [State:%v,Version:%v]\n", entry.Name, s.Value.State, s.Value.Version) + default: + return fmt.Errorf("Status returned unknown type: %T", s) + } + return nil +} diff --git a/x/ref/services/device/device/status_test.go b/x/ref/services/device/device/status_test.go new file mode 100644 index 000000000..29a56c3e3 --- /dev/null +++ b/x/ref/services/device/device/status_test.go @@ -0,0 +1,76 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "fmt" + "reflect" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/naming" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/test" + + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" +) + +func TestStatusCommand(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + + cmd := cmd_device.CmdRoot + addr := server.Status().Endpoints[0].String() + globName := naming.JoinAddressName(addr, "glob") + appName := naming.JoinAddressName(addr, "app") + + rootTape, appTape := tapes.ForSuffix(""), tapes.ForSuffix("app") + for _, c := range []struct { + tapeResponse device.Status + expected string + }{ + { + installationUninstalled, + fmt.Sprintf("Installation %v [State:Uninstalled,Version:director's cut]", appName), + }, + { + instanceUpdating, + fmt.Sprintf("Instance %v [State:Updating,Version:theatrical version]", appName), + }, + { + deviceService, + fmt.Sprintf("Device Service %v [State:Running,Version:han shot first]", appName), + }, + } { + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + tapes.Rewind() + rootTape.SetResponses(GlobResponse{results: []string{"app"}}) + appTape.SetResponses(c.tapeResponse) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{"status", globName}); err != nil { + t.Errorf("%v", err) + } + if expected, got := c.expected, strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected output from status. Got %q, expected %q", got, expected) + } + if got, expected := rootTape.Play(), []interface{}{GlobStimulus{"glob"}}; !reflect.DeepEqual(expected, got) { + t.Errorf("invalid call sequence. Got %v, want %v", got, expected) + } + if got, expected := appTape.Play(), []interface{}{"Status"}; !reflect.DeepEqual(expected, got) { + t.Errorf("invalid call sequence. Got %v, want %v", got, expected) + } + cmd_device.ResetGlobSettings() + } +} diff --git a/x/ref/services/device/device/uninstall.go b/x/ref/services/device/device/uninstall.go new file mode 100644 index 000000000..9beb224c6 --- /dev/null +++ b/x/ref/services/device/device/uninstall.go @@ -0,0 +1,38 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + + "v.io/v23/context" + "v.io/v23/services/device" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" +) + +var cmdUninstall = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runUninstall), + Name: "uninstall", + Short: "Uninstall the given application installation.", + Long: "Uninstall the given application installation.", + ArgsName: "<installation>", + ArgsLong: ` +<installation> is the vanadium object name of the application installation to +uninstall. +`, +} + +func runUninstall(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 1, len(args); expected != got { + return env.UsageErrorf("uninstall: incorrect number of arguments, expected %d, got %d", expected, got) + } + installName := args[0] + if err := device.ApplicationClient(installName).Uninstall(ctx); err != nil { + return fmt.Errorf("Uninstall failed: %v", err) + } + fmt.Fprintf(env.Stdout, "Successfully uninstalled: %q\n", installName) + return nil +} diff --git a/x/ref/services/device/device/update.go b/x/ref/services/device/device/update.go new file mode 100644 index 000000000..e05b41207 --- /dev/null +++ b/x/ref/services/device/device/update.go @@ -0,0 +1,161 @@ +// Copyright 2015 The Vanadium 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 main + +// TODO(caprita): Rename to update_revert.go + +import ( + "fmt" + "io" + "time" + + "v.io/v23/context" + "v.io/v23/services/device" + "v.io/v23/verror" + + "v.io/x/lib/cmdline" + "v.io/x/ref/services/device/internal/errors" +) + +var cmdUpdate = &cmdline.Command{ + Name: "update", + Short: "Update the device manager or applications.", + Long: "Update the device manager or application instances and installations", + ArgsName: "<name patterns...>", + ArgsLong: ` +<name patterns...> are vanadium object names or glob name patterns corresponding to the device manager service, or to application installations and instances.`, +} + +func init() { + globify(cmdUpdate, runUpdate, &GlobSettings{ + HandlerParallelism: KindParallelism, + InstanceStateFilter: ExcludeInstanceStates(device.InstanceStateDeleted), + InstallationStateFilter: ExcludeInstallationStates(device.InstallationStateUninstalled), + }) +} + +var cmdRevert = &cmdline.Command{ + Name: "revert", + Short: "Revert the device manager or applications.", + Long: "Revert the device manager or application instances and installations to a previous version of their current version", + ArgsName: "<name patterns...>", + ArgsLong: ` +<name patterns...> are vanadium object names or glob name patterns corresponding to the device manager service, or to application installations and instances.`, +} + +func init() { + globify(cmdRevert, runRevert, &GlobSettings{ + HandlerParallelism: KindParallelism, + InstanceStateFilter: ExcludeInstanceStates(device.InstanceStateDeleted), + InstallationStateFilter: ExcludeInstallationStates(device.InstallationStateUninstalled), + }) +} + +func instanceIsRunning(ctx *context.T, von string) (bool, error) { + status, err := device.ApplicationClient(von).Status(ctx) + if err != nil { + return false, fmt.Errorf("Failed to get status for instance %q: %v", von, err) + } + s, ok := status.(device.StatusInstance) + if !ok { + return false, fmt.Errorf("Status for instance %q of wrong type (%T)", von, status) + } + return s.Value.State == device.InstanceStateRunning, nil +} + +var revertOrUpdate = map[bool]string{true: "revert", false: "update"} +var revertOrUpdateMethod = map[bool]string{true: "Revert", false: "Update"} +var revertOrUpdateNoOp = map[bool]string{true: "no previous version available", false: "already up to date"} + +func changeVersionInstance(ctx *context.T, stdout, stderr io.Writer, name string, status device.StatusInstance, revert bool) (retErr error) { + if status.Value.State == device.InstanceStateRunning { + if err := device.ApplicationClient(name).Kill(ctx, killDeadline); err != nil { + // Check the app's state again in case we killed it, + // nevermind any errors. The sleep is because Kill + // currently (4/29/15) returns asynchronously with the + // device manager shooting the app down. + time.Sleep(time.Second) + running, rerr := instanceIsRunning(ctx, name) + if rerr != nil { + return rerr + } + if running { + return fmt.Errorf("Kill failed: %v", err) + } + fmt.Fprintf(stderr, "WARNING for \"%s\": recovered from Kill error (%s). Proceeding with %s.\n", name, err, revertOrUpdate[revert]) + } + // App was running, and we killed it, so we need to run it again + // after the update/revert. + defer func() { + if err := device.ApplicationClient(name).Run(ctx); err != nil { + err = fmt.Errorf("Run failed: %v", err) + if retErr == nil { + retErr = err + } else { + fmt.Fprintf(stderr, "ERROR for \"%s\": %v.\n", name, err) + } + } + }() + } + // Update/revert the instance. + var err error + if revert { + err = device.ApplicationClient(name).Revert(ctx) + } else { + err = device.ApplicationClient(name).Update(ctx) + } + switch { + case err == nil: + fmt.Fprintf(stdout, "Successful %s of version for instance \"%s\".\n", revertOrUpdate[revert], name) + return nil + case verror.ErrorID(err) == errors.ErrUpdateNoOp.ID: + // TODO(caprita): Ideally, we wouldn't even attempt a kill / + // restart if the update/revert is a no-op. + fmt.Fprintf(stdout, "Instance \"%s\": %s.\n", name, revertOrUpdateNoOp[revert]) + return nil + default: + return fmt.Errorf("%s failed: %v", revertOrUpdateMethod[revert], err) + } +} + +func changeVersionOne(ctx *context.T, what string, stdout, stderr io.Writer, name string, revert bool) error { + var err error + if revert { + err = device.ApplicationClient(name).Revert(ctx) + } else { + err = device.ApplicationClient(name).Update(ctx) + } + switch { + case err == nil: + fmt.Fprintf(stdout, "Successful %s of version for %s \"%s\".\n", revertOrUpdate[revert], what, name) + return nil + case verror.ErrorID(err) == errors.ErrUpdateNoOp.ID: + fmt.Fprintf(stdout, "%s \"%s\": %s.\n", what, name, revertOrUpdateNoOp[revert]) + return nil + default: + return fmt.Errorf("%s failed: %v", revertOrUpdateMethod[revert], err) + } +} + +func changeVersion(entry GlobResult, ctx *context.T, stdout, stderr io.Writer, revert bool) error { + switch entry.Kind { + case ApplicationInstanceObject: + return changeVersionInstance(ctx, stdout, stderr, entry.Name, entry.Status.(device.StatusInstance), revert) + case ApplicationInstallationObject: + return changeVersionOne(ctx, "installation", stdout, stderr, entry.Name, revert) + case DeviceServiceObject: + return changeVersionOne(ctx, "device service", stdout, stderr, entry.Name, revert) + default: + return fmt.Errorf("unhandled object kind %v", entry.Kind) + } +} + +func runUpdate(entry GlobResult, ctx *context.T, stdout, stderr io.Writer) error { + return changeVersion(entry, ctx, stdout, stderr, false) +} + +func runRevert(entry GlobResult, ctx *context.T, stdout, stderr io.Writer) error { + return changeVersion(entry, ctx, stdout, stderr, true) +} diff --git a/x/ref/services/device/device/update_test.go b/x/ref/services/device/device/update_test.go new file mode 100644 index 000000000..ca32285ba --- /dev/null +++ b/x/ref/services/device/device/update_test.go @@ -0,0 +1,159 @@ +// Copyright 2015 The Vanadium 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 main_test + +// TODO(caprita): Rename to update_revert_test.go + +import ( + "bytes" + "fmt" + "reflect" + "strings" + "testing" + "time" + "unicode" + "unicode/utf8" + + "v.io/v23" + "v.io/v23/naming" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/test" + + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" +) + +func capitalize(s string) string { + r, size := utf8.DecodeRuneInString(s) + if r == utf8.RuneError { + return "" + } + return string(unicode.ToUpper(r)) + s[size:] +} + +// TestUpdateAndRevertCommands verifies the device update and revert commands. +func TestUpdateAndRevertCommands(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + addr := server.Status().Endpoints[0].String() + root := cmd_device.CmdRoot + appName := naming.JoinAddressName(addr, "app") + rootTape := tapes.ForSuffix("") + globName := naming.JoinAddressName(addr, "glob") + // TODO(caprita): Move joinLines to a common place. + joinLines := func(args ...string) string { + return strings.Join(args, "\n") + } + for _, cmd := range []string{"update", "revert"} { + for _, c := range []struct { + globResponses []string + statusResponses map[string][]interface{} + expectedStimuli map[string][]interface{} + expectedStdout string + expectedStderr string + expectedError string + }{ + { // Everything succeeds. + []string{"app/2", "app/1", "app/5", "app/3", "app/4"}, + map[string][]interface{}{ + "app/1": []interface{}{instanceRunning, nil, nil, nil}, + "app/2": []interface{}{instanceNotRunning, nil}, + "app/3": []interface{}{installationActive, nil}, + // The uninstalled installation and the + // deleted instance should be excluded + // from the Update and Revert as per the + // default GlobSettings for the update + // and revert commands. + "app/4": []interface{}{installationUninstalled, nil}, + "app/5": []interface{}{instanceDeleted, nil}, + }, + map[string][]interface{}{ + "app/1": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, capitalize(cmd), "Run"}, + "app/2": []interface{}{"Status", capitalize(cmd)}, + "app/3": []interface{}{"Status", capitalize(cmd)}, + }, + joinLines( + fmt.Sprintf("Successful %s of version for installation \"%s/3\".", cmd, appName), + fmt.Sprintf("Successful %s of version for instance \"%s/1\".", cmd, appName), + fmt.Sprintf("Successful %s of version for instance \"%s/2\".", cmd, appName)), + "", + "", + }, + { // Assorted failure modes. + []string{"app/1", "app/2", "app/3", "app/4", "app/5"}, + map[string][]interface{}{ + // Starts as running, fails Kill, but then + // recovers. This ultimately counts as a success. + "app/1": []interface{}{instanceRunning, fmt.Errorf("Simulate Kill failing"), instanceNotRunning, nil, nil}, + // Starts as running, fails Kill, and stays running. + "app/2": []interface{}{instanceRunning, fmt.Errorf("Simulate Kill failing"), instanceRunning}, + // Starts as running, Kill and Update succeed, but Run fails. + "app/3": []interface{}{instanceRunning, nil, nil, fmt.Errorf("Simulate Run failing")}, + // Starts as running, Kill succeeds, Update fails, but Run succeeds. + "app/4": []interface{}{instanceRunning, nil, fmt.Errorf("Simulate %s failing", capitalize(cmd)), nil}, + // Starts as running, Kill succeeds, Update fails, and Run fails. + "app/5": []interface{}{instanceRunning, nil, fmt.Errorf("Simulate %s failing", capitalize(cmd)), fmt.Errorf("Simulate Run failing")}, + }, + map[string][]interface{}{ + "app/1": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, "Status", capitalize(cmd), "Run"}, + "app/2": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, "Status"}, + "app/3": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, capitalize(cmd), "Run"}, + "app/4": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, capitalize(cmd), "Run"}, + "app/5": []interface{}{"Status", KillStimulus{"Kill", 10 * time.Second}, capitalize(cmd), "Run"}, + }, + joinLines( + fmt.Sprintf("Successful %s of version for instance \"%s/1\".", cmd, appName), + fmt.Sprintf("Successful %s of version for instance \"%s/3\".", cmd, appName), + ), + joinLines( + fmt.Sprintf("WARNING for \"%s/1\": recovered from Kill error (device.test:<rpc.Client>\"%s/1\".Kill: Error: Simulate Kill failing). Proceeding with %s.", appName, appName, cmd), + fmt.Sprintf("ERROR for \"%s/2\": Kill failed: device.test:<rpc.Client>\"%s/2\".Kill: Error: Simulate Kill failing.", appName, appName), + fmt.Sprintf("ERROR for \"%s/3\": Run failed: device.test:<rpc.Client>\"%s/3\".Run: Error: Simulate Run failing.", appName, appName), + fmt.Sprintf("ERROR for \"%s/4\": %s failed: device.test:<rpc.Client>\"%s/4\".%s: Error: Simulate %s failing.", appName, capitalize(cmd), appName, capitalize(cmd), capitalize(cmd)), + fmt.Sprintf("ERROR for \"%s/5\": Run failed: device.test:<rpc.Client>\"%s/5\".Run: Error: Simulate Run failing.", appName, appName), + fmt.Sprintf("ERROR for \"%s/5\": %s failed: device.test:<rpc.Client>\"%s/5\".%s: Error: Simulate %s failing.", appName, capitalize(cmd), appName, capitalize(cmd), capitalize(cmd)), + ), + "encountered a total of 4 error(s)", + }, + } { + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + tapes.Rewind() + rootTape.SetResponses(GlobResponse{results: c.globResponses}) + for n, r := range c.statusResponses { + tapes.ForSuffix(n).SetResponses(r...) + } + args := []string{cmd, globName} + if err := v23cmd.ParseAndRunForTest(root, ctx, env, args); err != nil { + if want, got := c.expectedError, err.Error(); want != got { + t.Errorf("Unexpected error: want %v, got %v", want, got) + } + } else { + if c.expectedError != "" { + t.Errorf("Expected to get error %v, but didn't get any error.", c.expectedError) + } + } + + if expected, got := c.expectedStdout, strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected stdout output from %s.\nGot:\n%v\nExpected:\n%v", cmd, got, expected) + } + if expected, got := c.expectedStderr, strings.TrimSpace(stderr.String()); got != expected { + t.Errorf("Unexpected stderr output from %s.\nGot:\n%v\nExpected:\n%v", cmd, got, expected) + } + for n, m := range c.expectedStimuli { + if want, got := m, tapes.ForSuffix(n).Play(); !reflect.DeepEqual(want, got) { + t.Errorf("Unexpected stimuli for %v. Want: %v, got %v.", n, want, got) + } + } + cmd_device.ResetGlobSettings() + } + } +} diff --git a/x/ref/services/device/device/util_test.go b/x/ref/services/device/device/util_test.go new file mode 100644 index 000000000..167e56b7a --- /dev/null +++ b/x/ref/services/device/device/util_test.go @@ -0,0 +1,126 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "reflect" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/naming" + "v.io/v23/services/device" + "v.io/v23/verror" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + cmd_device "v.io/x/ref/services/device/device" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/test" +) + +var ( + installationUninstalled = device.StatusInstallation{device.InstallationStatus{ + State: device.InstallationStateUninstalled, + Version: "director's cut", + }} + installationActive = device.StatusInstallation{device.InstallationStatus{ + State: device.InstallationStateActive, + Version: "extended cut", + }} + instanceUpdating = device.StatusInstance{device.InstanceStatus{ + State: device.InstanceStateUpdating, + Version: "theatrical version", + }} + instanceRunning = device.StatusInstance{device.InstanceStatus{ + State: device.InstanceStateRunning, + Version: "tv version", + }} + instanceNotRunning = device.StatusInstance{device.InstanceStatus{ + State: device.InstanceStateNotRunning, + Version: "special edition", + }} + instanceDeleted = device.StatusInstance{device.InstanceStatus{ + State: device.InstanceStateDeleted, + Version: "mini series", + }} + deviceService = device.StatusDevice{device.DeviceStatus{ + State: device.InstanceStateRunning, + Version: "han shot first", + }} + deviceUpdating = device.StatusDevice{device.DeviceStatus{ + State: device.InstanceStateUpdating, + Version: "international release", + }} +) + +func testHelper(t *testing.T, lower, upper string) { + ctx, shutdown := test.V23Init() + defer shutdown() + + tapes := servicetest.NewTapeMap() + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", newDispatcher(t, tapes)) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + addr := server.Status().Endpoints[0].String() + + // Setup the command-line. + cmd := cmd_device.CmdRoot + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + appName := naming.JoinAddressName(addr, "appname") + + // Confirm that we correctly enforce the number of arguments. + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{lower}); err == nil { + t.Fatalf("wrongly failed to receive a non-nil error.") + } + if expected, got := "ERROR: "+lower+": incorrect number of arguments, expected 1, got 0", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from %s. Got %q, expected prefix %q", lower, got, expected) + } + stdout.Reset() + stderr.Reset() + appTape := tapes.ForSuffix("appname") + appTape.Rewind() + + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{lower, "nope", "nope"}); err == nil { + t.Fatalf("wrongly failed to receive a non-nil error.") + } + if expected, got := "ERROR: "+lower+": incorrect number of arguments, expected 1, got 2", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Fatalf("Unexpected output from %s. Got %q, expected prefix %q", lower, got, expected) + } + stdout.Reset() + stderr.Reset() + appTape.Rewind() + + // Correct operation. + appTape.SetResponses(nil) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{lower, appName}); err != nil { + t.Fatalf("%s failed when it shouldn't: %v", lower, err) + } + if expected, got := upper+" succeeded", strings.TrimSpace(stdout.String()); got != expected { + t.Fatalf("Unexpected output from %s. Got %q, expected %q", lower, got, expected) + } + if expected, got := []interface{}{upper}, appTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("invalid call sequence. Got %v, want %v", got, expected) + } + appTape.Rewind() + stderr.Reset() + stdout.Reset() + + // Test with bad parameters. + appTape.SetResponses(verror.New(errOops, nil)) + if err := v23cmd.ParseAndRunForTest(cmd, ctx, env, []string{lower, appName}); err == nil { + t.Fatalf("wrongly didn't receive a non-nil error.") + } + // expected the same. + if expected, got := []interface{}{upper}, appTape.Play(); !reflect.DeepEqual(expected, got) { + t.Errorf("invalid call sequence. Got %v, want %v", got, expected) + } +} + +func joinLines(args ...string) string { + return strings.Join(args, "\n") +} diff --git a/x/ref/services/device/deviced/commands.go b/x/ref/services/device/deviced/commands.go new file mode 100644 index 000000000..6756453f6 --- /dev/null +++ b/x/ref/services/device/deviced/commands.go @@ -0,0 +1,169 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "fmt" + "os" + + "v.io/v23/context" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/services/device/deviced/internal/impl" + "v.io/x/ref/services/device/deviced/internal/installer" +) + +var ( + installFrom string + suidHelper string + restarter string + agent string + initHelper string + devUserName string + origin string + singleUser bool + sessionMode bool + initMode bool +) + +const deviceDirEnv = "V23_DEVICE_DIR" + +func installationDir(ctx *context.T, env *cmdline.Env) string { + if d := env.Vars[deviceDirEnv]; d != "" { + return d + } + if d, err := os.Getwd(); err != nil { + ctx.Errorf("Failed to get current dir: %v", err) + return "" + } else { + return d + } +} + +var cmdInstall = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runInstall), + Name: "install", + Short: "Install the device manager.", + Long: fmt.Sprintf("Performs installation of device manager into %s (if the env var set), or into the current dir otherwise", deviceDirEnv), + ArgsName: "[-- <arguments for device manager>]", + ArgsLong: ` +Arguments to be passed to the installed device manager`, +} + +func init() { + cmdInstall.Flags.StringVar(&installFrom, "from", "", "if specified, performs the installation from the provided application envelope object name") + cmdInstall.Flags.StringVar(&suidHelper, "suid_helper", "", "path to suid helper") + cmdInstall.Flags.StringVar(&restarter, "restarter", "", "path to restarter") + cmdInstall.Flags.StringVar(&agent, "agent", "", "path to security agent") + cmdInstall.Flags.StringVar(&initHelper, "init_helper", "", "path to sysinit helper") + cmdInstall.Flags.StringVar(&origin, "origin", "", "if specified, self-updates will use this origin") + cmdInstall.Flags.StringVar(&devUserName, "devuser", "", "if specified, device manager will run as this user. Provided by devicex but ignored .") + cmdInstall.Flags.BoolVar(&singleUser, "single_user", false, "if set, performs the installation assuming a single-user system") + cmdInstall.Flags.BoolVar(&sessionMode, "session_mode", false, "if set, installs the device manager to run a single session. Otherwise, the device manager is configured to get restarted upon exit") + cmdInstall.Flags.BoolVar(&initMode, "init_mode", false, "if set, installs the device manager with the system init service manager") +} + +func runInstall(ctx *context.T, env *cmdline.Env, args []string) error { + if installFrom != "" { + // TODO(caprita): Also pass args into InstallFrom. + if err := installer.InstallFrom(installFrom); err != nil { + ctx.Errorf("InstallFrom(%v) failed: %v", installFrom, err) + return err + } + return nil + } + if suidHelper == "" { + return env.UsageErrorf("--suid_helper must be set") + } + if restarter == "" { + return env.UsageErrorf("--restarter must be set") + } + if agent == "" { + return env.UsageErrorf("--agent must be set") + } + if initMode && initHelper == "" { + return env.UsageErrorf("--init_helper must be set") + } + if err := installer.SelfInstall(ctx, installationDir(ctx, env), suidHelper, restarter, agent, initHelper, origin, singleUser, sessionMode, initMode, args, os.Environ(), env.Stderr, env.Stdout); err != nil { + ctx.Errorf("SelfInstall failed: %v", err) + return err + } + return nil +} + +var cmdUninstall = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runUninstall), + Name: "uninstall", + Short: "Uninstall the device manager.", + Long: fmt.Sprintf("Removes the device manager installation from %s (if the env var set), or the current dir otherwise", deviceDirEnv), +} + +func init() { + cmdUninstall.Flags.StringVar(&suidHelper, "suid_helper", "", "path to suid helper") +} + +func runUninstall(ctx *context.T, env *cmdline.Env, _ []string) error { + if suidHelper == "" { + return env.UsageErrorf("--suid_helper must be set") + } + if err := installer.Uninstall(ctx, installationDir(ctx, env), suidHelper, env.Stderr, env.Stdout); err != nil { + ctx.Errorf("Uninstall failed: %v", err) + return err + } + return nil +} + +var cmdStart = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runStart), + Name: "start", + Short: "Start the device manager.", + Long: fmt.Sprintf("Starts the device manager installed under from %s (if the env var set), or the current dir otherwise", deviceDirEnv), +} + +func runStart(ctx *context.T, env *cmdline.Env, _ []string) error { + if err := installer.Start(ctx, installationDir(ctx, env), env.Stderr, env.Stdout); err != nil { + ctx.Errorf("Start failed: %v", err) + return err + } + return nil +} + +var cmdStop = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runStop), + Name: "stop", + Short: "Stop the device manager.", + Long: fmt.Sprintf("Stops the device manager installed under from %s (if the env var set), or the current dir otherwise", deviceDirEnv), +} + +func runStop(ctx *context.T, env *cmdline.Env, _ []string) error { + if err := installer.Stop(ctx, installationDir(ctx, env), env.Stderr, env.Stdout); err != nil { + ctx.Errorf("Stop failed: %v", err) + return err + } + return nil +} + +var cmdProfile = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runProfile), + Name: "profile", + Short: "Dumps profile for the device manager.", + Long: "Prints the internal profile description for the device manager.", +} + +func runProfile(ctx *context.T, env *cmdline.Env, _ []string) error { + spec, err := impl.ComputeDeviceProfile() + if err != nil { + ctx.Errorf("ComputeDeviceProfile failed: %v", err) + return err + } + fmt.Fprintf(env.Stdout, "Profile: %#v\n", spec) + desc, err := impl.Describe() + if err != nil { + ctx.Errorf("Describe failed: %v", err) + return err + } + fmt.Fprintf(env.Stdout, "Description: %#v\n", desc) + return nil +} diff --git a/x/ref/services/device/deviced/doc.go b/x/ref/services/device/deviced/doc.go new file mode 100644 index 000000000..7a334d175 --- /dev/null +++ b/x/ref/services/device/deviced/doc.go @@ -0,0 +1,217 @@ +// Copyright 2018 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated via go generate. +// DO NOT UPDATE MANUALLY + +/* +Command deviced is used to launch, configure and manage the deviced daemon, +which implements the v.io/v23/services/device interfaces. + +Usage: + deviced [flags] + deviced [flags] <command> + +The deviced commands are: + install Install the device manager. + uninstall Uninstall the device manager. + start Start the device manager. + stop Stop the device manager. + profile Dumps profile for the device manager. + help Display help for commands or topics + +The global flags are: + -deviced-port=0 + the port number of assign to the device manager service. The hostname/IP + address part of --v23.tcp.address is used along with this port. By default, + the port is assigned by the OS. + -name= + name to publish the device manager at + -neighborhood-name= + if provided, it will enable sharing with the local neighborhood with the + provided name. The address of the local mounttable will be published to the + neighboorhood and everything in the neighborhood will be visible on the local + mounttable. + -restart-exit-code=0 + exit code to return when device manager should be restarted + -use-pairing-token=false + generate a pairing token for the device manager that will need to be provided + when a device is claimed + + -agentsock= + Path to the application's security agent socket. + -alsologtostderr=true + log to standard error as well as files + -chown=false + Change owner of files and directories given as command-line arguments to the + user specified by this flag + -dryrun=false + Elides root-requiring systemcalls. + -kill=false + Kill process ids given as command-line arguments. + -log_backtrace_at=:0 + when logging hits line file:N, emit a stack trace + -log_dir= + if non-empty, write log files to this directory + -logdir= + Path to the log directory. + -logtostderr=false + log to standard error instead of files + -max_stack_buf_size=4292608 + max size in bytes of the buffer to use for logging stack traces + -metadata=<just specify -metadata to activate> + Displays metadata for the program and exits. + -minuid=501 + UIDs cannot be less than this number. + -progname=unnamed_app + Visible name of the application, used in argv[0] + -rm=false + Remove the file trees given as command-line arguments. + -run= + Path to the application to exec. + -stderrthreshold=2 + logs at or above this threshold go to stderr + -time=false + Dump timing information to stderr before exiting the program. + -username= + The UNIX user name used for the other functions of this tool. + -v=0 + log level for V logs + -v23.credentials= + directory to use for storing security credentials + -v23.i18n-catalogue= + 18n catalogue files to load, comma separated + -v23.namespace.root=[/(dev.v.io:r:vprod:service:mounttabled)@ns.dev.v.io:8101] + local namespace root; can be repeated to provided multiple roots + -v23.permissions.file= + specify a perms file as <name>:<permsfile> + -v23.permissions.literal= + explicitly specify the runtime perms as a JSON-encoded access.Permissions. + Overrides all --v23.permissions.file flags + -v23.proxy= + object name of proxy service to use to export services across network + boundaries + -v23.tcp.address= + address to listen on + -v23.tcp.protocol= + protocol to listen with + -v23.vtrace.cache-size=1024 + The number of vtrace traces to store in memory + -v23.vtrace.collect-regexp= + Spans and annotations that match this regular expression will trigger trace + collection + -v23.vtrace.dump-on-shutdown=true + If true, dump all stored traces on runtime shutdown + -v23.vtrace.sample-rate=0 + Rate (from 0.0 to 1.0) to sample vtrace traces + -v23.vtrace.v=0 + The verbosity level of the log messages to be captured in traces + -vmodule= + comma-separated list of globpattern=N settings for filename-filtered logging + (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns baz or + *az or b* but not by bar/baz or baz.go or az or b.* + -vpath= + comma-separated list of regexppattern=N settings for file pathname-filtered + logging (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns + foo/bar/baz or fo.*az or oo/ba or b.z but not by foo/bar/baz.go or fo*az + -workspace= + Path to the application's workspace directory. + +Deviced install + +Performs installation of device manager into V23_DEVICE_DIR (if the env var +set), or into the current dir otherwise + +Usage: + deviced install [flags] [-- <arguments for device manager>] + +Arguments to be passed to the installed device manager + +The deviced install flags are: + -agent= + path to security agent + -devuser= + if specified, device manager will run as this user. Provided by devicex but + ignored . + -from= + if specified, performs the installation from the provided application + envelope object name + -init_helper= + path to sysinit helper + -init_mode=false + if set, installs the device manager with the system init service manager + -origin= + if specified, self-updates will use this origin + -restarter= + path to restarter + -session_mode=false + if set, installs the device manager to run a single session. Otherwise, the + device manager is configured to get restarted upon exit + -single_user=false + if set, performs the installation assuming a single-user system + -suid_helper= + path to suid helper + +Deviced uninstall + +Removes the device manager installation from V23_DEVICE_DIR (if the env var +set), or the current dir otherwise + +Usage: + deviced uninstall [flags] + +The deviced uninstall flags are: + -suid_helper= + path to suid helper + +Deviced start + +Starts the device manager installed under from V23_DEVICE_DIR (if the env var +set), or the current dir otherwise + +Usage: + deviced start [flags] + +Deviced stop + +Stops the device manager installed under from V23_DEVICE_DIR (if the env var +set), or the current dir otherwise + +Usage: + deviced stop [flags] + +Deviced profile + +Prints the internal profile description for the device manager. + +Usage: + deviced profile [flags] + +Deviced help - Display help for commands or topics + +Help with no args displays the usage of the parent command. + +Help with args displays the usage of the specified sub-command or help topic. + +"help ..." recursively displays help for all commands and topics. + +Usage: + deviced help [flags] [command/topic ...] + +[command/topic ...] optionally identifies a specific sub-command or help topic. + +The deviced help flags are: + -style=compact + The formatting style for help output: + compact - Good for compact cmdline output. + full - Good for cmdline output, shows all global flags. + godoc - Good for godoc processing. + shortonly - Only output short description. + Override the default by setting the CMDLINE_STYLE environment variable. + -width=<terminal width> + Format output to this target width in runes, or unlimited if width < 0. + Defaults to the terminal width if available. Override the default by setting + the CMDLINE_WIDTH environment variable. +*/ +package main diff --git a/x/ref/services/device/deviced/internal/impl/app_service.go b/x/ref/services/device/deviced/internal/impl/app_service.go new file mode 100644 index 000000000..d28562f57 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/app_service.go @@ -0,0 +1,1692 @@ +// Copyright 2015 The Vanadium 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 impl + +// The app invoker is responsible for managing the state of applications on the +// device manager. The device manager manages the applications it installs and +// runs using the following directory structure. Permissions and owners are +// noted as parentheses enclosed octal perms with an 'a' or 'd' suffix for app +// or device manager respectively. For example: (755d) +// +// TODO(caprita): Not all is yet implemented. +// +// <config.Root>(711d)/ +// app-<hash 1>(711d)/ - the application dir is named using a hash of the application title +// installation-<id 1>(711d)/ - installations are labelled with ids +// acls(700d)/ +// data(700d) - the AccessList data for this +// installation. Controls access to +// Instantiate, Uninstall, Update, +// UpdateTo and Revert. +// signature(700d) - the signature for the AccessLists in data +// <status>(700d) - one of the values for InstallationState enum +// origin(700d) - object name for application envelope +// config(700d) - Config provided by the installer +// packages(700d) - set of packages specified by the installer +// pkg(700d)/ - downloaded packages +// <pkg name>(700d) +// <pkg name>.__info(700d) +// ... +// <version 1 timestamp>(711d)/ - timestamp of when the version was downloaded +// bin(755d) - application binary +// previous - symbolic link to previous version directory +// envelope - application envelope (JSON-encoded) +// packages(755d)/ - installed packages (from envelope+installer) +// <pkg name>(755d)/ +// ... +// <version 2 timestamp>(711d) +// ... +// current - symbolic link to the current version +// instances(711d)/ +// instance-<id a>(711d)/ - instances are labelled with ids +// credentials(700d)/ - holds vanadium credentials (unless running +// through security agent) +// root(700a)/ - workspace that the instance is run from +// packages - symbolic link to version's packages +// logs(755a)/ - stderr/stdout and log files generated by instance +// info(700d) - metadata for the instance (such as app +// cycle manager name and process id) +// installation - symbolic link to installation for the instance +// version - symbolic link to installation version for the instance +// agent-sock-dir - symbolic link to the agent socket dir +// acls(700d)/ +// data(700d) - the AccessLists for this instance. These +// AccessLists control access to Run, +// Kill and Delete. +// signature(700d) - the signature for these AccessLists. +// <status>(700d) - one of the values for InstanceState enum +// systemname(700d) - the system name used to execute this instance +// debugacls (711d)/ +// data(644)/ - the Permissions for Debug access to the application. Shared +// with the application. +// signature(644)/ - the signature for these Permissions. +// instance-<id b>(711d) +// ... +// installation-<id 2>(711d) +// ... +// app-<hash 2>(711d) +// ... +// +// The device manager uses the suid helper binary to invoke an application as a +// specified user. The path to the helper is specified as config.Helper. + +// When device manager starts up, it goes through all instances and launches the +// ones that are not running. If an instance fails to launch, it stays not +// running. +// +// Instantiate creates an instance. Run launches the process. Kill kills the +// process but leaves the workspace untouched. Delete prevents future launches +// (it also eventually gc's the workspace, logs, and other instance state). +// +// If the process dies on its own, it stays dead and is assumed not running. +// TODO(caprita): Later, we'll add auto-restart option. +// +// Concurrency model: installations can be created independently of one another; +// installations can be removed at any time (TODO(caprita): ensure all instances +// are Deleted). The first call to Uninstall will rename the installation dir +// as a first step; subsequent Uninstall's will fail. Instances can be created +// independently of one another, as long as the installation exists (if it gets +// Uninstall'ed during a Instantiate, the Instantiate call may fail). +// +// The status file present in each instance is used to flag the state of the +// instance and prevent concurrent operations against the instance: +// +// - when an instance is created with Instantiate, it is placed in state +// 'not-running'. +// +// - Run attempts to transition from 'not-running' to 'launching' (if the +// instance was not in 'not-running' state, Run fails). From 'launching', the +// instance transitions to 'running' upon success or back to 'not-running' upon +// failure. +// +// - Kill attempts to transition from 'running' to 'dying' (if the +// instance was not in 'running' state, Kill fails). From 'dying', the +// instance transitions to 'not-running' upon success or back to 'running' upon +// failure. +// +// - Delete transitions from 'not-running' to 'deleted'. If the initial +// state is not 'not-running', Delete fails. +// +// TODO(caprita): There is room for synergy between how device manager organizes +// its own workspace and that for the applications it runs. In particular, +// previous, origin, and envelope could be part of a single config. We'll +// refine that later. + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "reflect" + "regexp" + "strconv" + "strings" + "text/template" + "time" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/glob" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/appcycle" + "v.io/v23/services/application" + "v.io/v23/services/device" + "v.io/v23/verror" + "v.io/v23/vom" + "v.io/x/ref" + vexec "v.io/x/ref/lib/exec" + "v.io/x/ref/lib/mgmt" + "v.io/x/ref/services/agent" + "v.io/x/ref/services/device/internal/config" + "v.io/x/ref/services/device/internal/errors" + "v.io/x/ref/services/internal/packages" + "v.io/x/ref/services/internal/pathperms" +) + +// instanceInfo holds state about an instance. +type instanceInfo struct { + AppCycleMgrName string + Pid int + + // Blessings to provide the AppCycleManager in the app with so that it can talk + // to the device manager. + AppCycleBlessings string + Restarts int32 + RestartWindowBegan time.Time +} + +func saveInstanceInfo(ctx *context.T, dir string, info *instanceInfo) error { + jsonInfo, err := json.Marshal(info) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Marshal(%v) failed: %v", info, err)) + } + infoPath := filepath.Join(dir, "info") + if err := ioutil.WriteFile(infoPath, jsonInfo, 0600); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("WriteFile(%v) failed: %v", infoPath, err)) + } + return nil +} + +func loadInstanceInfo(ctx *context.T, dir string) (*instanceInfo, error) { + infoPath := filepath.Join(dir, "info") + info := new(instanceInfo) + if infoBytes, err := ioutil.ReadFile(infoPath); err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("ReadFile(%v) failed: %v", infoPath, err)) + } else if err := json.Unmarshal(infoBytes, info); err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Unmarshal(%v) failed: %v", infoBytes, err)) + } + return info, nil +} + +// appRunner is the subset of the appService object needed to +// (re)start an application. +type appRunner struct { + callback *callbackState + // principalMgr handles principals for the apps. + principalMgr principalManager + // reap is the app process monitoring subsystem. + reap *reaper + // mtAddress is the address of the local mounttable. + mtAddress string + // appServiceName is a name by which the appService can be reached + appServiceName string + stats *stats +} + +// appService implements the Device manager's Application interface. +type appService struct { + config *config.State + // suffix contains the name components of the current invocation name + // suffix. It is used to identify an application, installation, or + // instance. + suffix []string + uat BlessingSystemAssociationStore + permsStore *pathperms.PathStore + // Reference to the devicemanager top-level AccessList list. + deviceAccessList access.Permissions + // State needed to (re)start an application. + runner *appRunner + stats *stats +} + +func saveEnvelope(ctx *context.T, dir string, envelope *application.Envelope) error { + jsonEnvelope, err := json.Marshal(envelope) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Marshal(%v) failed: %v", envelope, err)) + } + path := filepath.Join(dir, "envelope") + if err := ioutil.WriteFile(path, jsonEnvelope, 0600); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("WriteFile(%v) failed: %v", path, err)) + } + return nil +} + +func loadEnvelope(ctx *context.T, dir string) (*application.Envelope, error) { + path := filepath.Join(dir, "envelope") + envelope := new(application.Envelope) + if envelopeBytes, err := ioutil.ReadFile(path); err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("ReadFile(%v) failed: %v", path, err)) + } else if err := json.Unmarshal(envelopeBytes, envelope); err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Unmarshal(%v) failed: %v", envelopeBytes, err)) + } + return envelope, nil +} + +func loadEnvelopeForInstance(ctx *context.T, instanceDir string) (*application.Envelope, error) { + versionLink := filepath.Join(instanceDir, "version") + versionDir, err := filepath.EvalSymlinks(versionLink) + if err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", versionLink, err)) + } + return loadEnvelope(ctx, versionDir) +} + +func saveConfig(ctx *context.T, dir string, config device.Config) error { + jsonConfig, err := json.Marshal(config) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Marshal(%v) failed: %v", config, err)) + } + path := filepath.Join(dir, "config") + if err := ioutil.WriteFile(path, jsonConfig, 0600); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("WriteFile(%v) failed: %v", path, err)) + } + return nil +} + +func loadConfig(ctx *context.T, dir string) (device.Config, error) { + path := filepath.Join(dir, "config") + var config device.Config + if configBytes, err := ioutil.ReadFile(path); err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("ReadFile(%v) failed: %v", path, err)) + } else if err := json.Unmarshal(configBytes, &config); err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Unmarshal(%v) failed: %v", configBytes, err)) + } + return config, nil +} + +func savePackages(ctx *context.T, dir string, packages application.Packages) error { + jsonPackages, err := json.Marshal(packages) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Marshal(%v) failed: %v", packages, err)) + } + path := filepath.Join(dir, "packages") + if err := ioutil.WriteFile(path, jsonPackages, 0600); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("WriteFile(%v) failed: %v", path, err)) + } + return nil +} + +func loadPackages(ctx *context.T, dir string) (application.Packages, error) { + path := filepath.Join(dir, "packages") + var packages application.Packages + if packagesBytes, err := ioutil.ReadFile(path); err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("ReadFile(%v) failed: %v", path, err)) + } else if err := json.Unmarshal(packagesBytes, &packages); err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Unmarshal(%v) failed: %v", packagesBytes, err)) + } + return packages, nil +} + +func saveOrigin(ctx *context.T, dir, originVON string) error { + path := filepath.Join(dir, "origin") + if err := ioutil.WriteFile(path, []byte(originVON), 0600); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("WriteFile(%v) failed: %v", path, err)) + } + return nil +} + +func loadOrigin(ctx *context.T, dir string) (string, error) { + path := filepath.Join(dir, "origin") + if originBytes, err := ioutil.ReadFile(path); err != nil { + return "", verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("ReadFile(%v) failed: %v", path, err)) + } else { + return string(originBytes), nil + } +} + +// generateID returns a new unique id string. The uniqueness is based on the +// current timestamp. Not cryptographically secure. +func generateID() string { + const timeFormat = "20060102-15:04:05.0000" + return time.Now().UTC().Format(timeFormat) +} + +// TODO(caprita): Nothing prevents different applications from sharing the same +// title, and thereby being installed in the same app dir. Do we want to +// prevent that for the same user or across users? + +const ( + appDirPrefix = "app-" + installationPrefix = "installation-" + instancePrefix = "instance-" +) + +// applicationDirName generates a cryptographic hash of the application title, +// to be used as a directory name for installations of the application with the +// given title. +func applicationDirName(title string) string { + h := md5.New() + h.Write([]byte(title)) + hash := strings.TrimRight(base64.URLEncoding.EncodeToString(h.Sum(nil)), "=") + return appDirPrefix + hash +} + +func installationDirName(installationID string) string { + return installationPrefix + installationID +} + +func instanceDirName(instanceID string) string { + return instancePrefix + instanceID +} + +func mkdir(ctx *context.T, dir string) error { + return mkdirPerm(ctx, dir, 0700) +} + +func mkdirPerm(ctx *context.T, dir string, permissions int) error { + perm := os.FileMode(permissions) + if err := os.MkdirAll(dir, perm); err != nil { + ctx.Errorf("MkdirAll(%v, %v) failed: %v", dir, perm, err) + return err + } + return nil +} + +func sockPath(instanceDir string) (string, error) { + sockLink := filepath.Join(instanceDir, "agent-sock-dir") + sock, err := filepath.EvalSymlinks(sockLink) + if err != nil { + return "", err + } + return filepath.Join(sock, "s"), nil +} + +func fetchAppEnvelope(ctx *context.T, origin string) (*application.Envelope, error) { + envelope, err := fetchEnvelope(ctx, origin) + if err != nil { + return nil, err + } + if envelope.Title == application.DeviceManagerTitle { + // Disallow device manager apps from being installed like a + // regular app. + return nil, verror.New(errors.ErrInvalidOperation, ctx, "DeviceManager apps cannot be installed") + } + return envelope, nil +} + +// newVersion sets up the directory for a new application version. +func newVersion(ctx *context.T, installationDir string, envelope *application.Envelope, oldVersionDir string) (string, error) { + versionDir := filepath.Join(installationDir, generateVersionDirName()) + if err := mkdirPerm(ctx, versionDir, 0711); err != nil { + return "", verror.New(errors.ErrOperationFailed, ctx, err) + } + if err := saveEnvelope(ctx, versionDir, envelope); err != nil { + return versionDir, err + } + pkgDir := filepath.Join(versionDir, "pkg") + if err := mkdir(ctx, pkgDir); err != nil { + return "", verror.New(errors.ErrOperationFailed, ctx, err) + } + publisher := envelope.Publisher + // TODO(caprita): Share binaries if already existing locally. + if err := downloadBinary(ctx, publisher, &envelope.Binary, versionDir, "bin"); err != nil { + return versionDir, err + } + if err := downloadPackages(ctx, publisher, envelope.Packages, pkgDir); err != nil { + return versionDir, err + } + if err := installPackages(ctx, installationDir, versionDir); err != nil { + return versionDir, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("installPackages(%v, %v) failed: %v", installationDir, versionDir, err)) + } + if err := os.RemoveAll(pkgDir); err != nil { + return versionDir, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("RemoveAll(%v) failed: %v", pkgDir, err)) + } + if oldVersionDir != "" { + previousLink := filepath.Join(versionDir, "previous") + if err := os.Symlink(oldVersionDir, previousLink); err != nil { + return versionDir, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Symlink(%v, %v) failed: %v", oldVersionDir, previousLink, err)) + } + } + // UpdateLink should be the last thing we do, after we've ensured the + // new version is viable (currently, that just means it installs + // properly). + return versionDir, UpdateLink(versionDir, filepath.Join(installationDir, "current")) +} + +func (i *appService) Install(ctx *context.T, call rpc.ServerCall, applicationVON string, config device.Config, packages application.Packages) (string, error) { + if len(i.suffix) > 0 { + return "", verror.New(errors.ErrInvalidSuffix, ctx) + } + ctx, cancel := context.WithTimeout(ctx, rpcContextLongTimeout) + defer cancel() + envelope, err := fetchAppEnvelope(ctx, applicationVON) + if err != nil { + return "", err + } + installationID := generateID() + installationDir := filepath.Join(i.config.Root, applicationDirName(envelope.Title), installationDirName(installationID)) + deferrer := func() { + CleanupDir(ctx, installationDir, "") + } + if err := mkdirPerm(ctx, installationDir, 0711); err != nil { + return "", verror.New(errors.ErrOperationFailed, nil) + } + defer func() { + if deferrer != nil { + deferrer() + } + }() + if newOrigin, ok := config[mgmt.AppOriginConfigKey]; ok { + delete(config, mgmt.AppOriginConfigKey) + applicationVON = newOrigin + } + if err := saveOrigin(ctx, installationDir, applicationVON); err != nil { + return "", err + } + if err := saveConfig(ctx, installationDir, config); err != nil { + return "", err + } + if err := savePackages(ctx, installationDir, packages); err != nil { + return "", err + } + pkgDir := filepath.Join(installationDir, "pkg") + if err := mkdir(ctx, pkgDir); err != nil { + return "", verror.New(errors.ErrOperationFailed, ctx, err) + } + // We use a zero value publisher, meaning that any signatures present in the + // package files are not verified. + // TODO(caprita): Issue warnings when signatures are present and ignored. + if err := downloadPackages(ctx, security.Blessings{}, packages, pkgDir); err != nil { + return "", err + } + if _, err := newVersion(ctx, installationDir, envelope, ""); err != nil { + return "", err + } + // TODO(caprita,rjkroege): Should the installation AccessLists really be + // seeded with the device AccessList? Instead, might want to hide the deviceAccessList + // from the app? + blessings, _ := security.RemoteBlessingNames(ctx, call.Security()) + if err := i.initializeSubAccessLists(installationDir, blessings, i.deviceAccessList.Copy()); err != nil { + return "", err + } + if err := initializeInstallation(installationDir, device.InstallationStateActive); err != nil { + return "", err + } + deferrer = nil + // TODO(caprita): Using the title without cleaning out slashes + // introduces extra name components that mess up the device manager's + // apps object space. We should fix this either by santizing the title, + // or disallowing slashes in titles to begin with. + return naming.Join(envelope.Title, installationID), nil +} + +func openWriteFile(path string) (*os.File, error) { + perm := os.FileMode(0644) + return os.OpenFile(path, os.O_WRONLY|os.O_CREATE, perm) +} + +// TODO(gauthamt): Make sure we pass the context to installationDirCore. +func installationDirCore(components []string, root string) (string, error) { + if nComponents := len(components); nComponents != 2 { + return "", verror.New(errors.ErrInvalidSuffix, nil) + } + app, installation := components[0], components[1] + installationDir := filepath.Join(root, applicationDirName(app), installationDirName(installation)) + if _, err := os.Stat(installationDir); err != nil { + if os.IsNotExist(err) { + return "", verror.New(verror.ErrNoExist, nil, naming.Join(components...)) + } + return "", verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("Stat(%v) failed: %v", installationDir, err)) + } + return installationDir, nil +} + +// setupPrincipal sets up the instance's principal, with the right blessings. +func setupPrincipal(ctx *context.T, instanceDir string, call device.ApplicationInstantiateServerCall, principalMgr principalManager, info *instanceInfo, rootDir string) error { + if err := principalMgr.Create(instanceDir); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Create(%v) failed: %v", instanceDir, err)) + } + if err := principalMgr.Serve(instanceDir, nil); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Serve(%v) failed: %v", instanceDir, err)) + } + defer principalMgr.StopServing(instanceDir) + p, err := principalMgr.Load(instanceDir) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Load(%v) failed: %v", instanceDir, err)) + } + defer p.Close() + + mPubKey, err := p.PublicKey().MarshalBinary() + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("PublicKey().MarshalBinary() failed: %v", err)) + } + if err := call.SendStream().Send(device.BlessServerMessageInstancePublicKey{Value: mPubKey}); err != nil { + return err + } + if !call.RecvStream().Advance() { + return verror.New(errors.ErrInvalidBlessing, ctx, fmt.Sprintf("no blessings on stream: %v", call.RecvStream().Err())) + } + msg := call.RecvStream().Value() + appBlessingsFromInstantiator, ok := msg.(device.BlessClientMessageAppBlessings) + if !ok { + return verror.New(errors.ErrInvalidBlessing, ctx, fmt.Sprintf("wrong message type: %#v", msg)) + } + // Should we move this after the addition of publisher blessings, and thus allow + // apps to run with only publisher blessings? + if appBlessingsFromInstantiator.Value.IsZero() { + return verror.New(errors.ErrInvalidBlessing, ctx) + } + if err := p.BlessingStore().SetDefault(appBlessingsFromInstantiator.Value); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("BlessingStore.SetDefault() failed: %v", err)) + } + // If there were any publisher blessings in the envelope, add those to the set of blessings + // sent to servers by default + appBlessings, err := addPublisherBlessings(ctx, instanceDir, p, appBlessingsFromInstantiator.Value) + if _, err := p.BlessingStore().Set(appBlessings, security.AllPrincipals); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("BlessingStore.Set() failed: %v", err)) + } + if err := security.AddToRoots(p, appBlessings); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("AddToRoots() failed: %v", err)) + } + // In addition, we give the app separate blessings for the purpose of + // communicating with the device manager. + info.AppCycleBlessings, err = createCallbackBlessings(ctx, p.PublicKey()) + return err +} + +func addPublisherBlessings(ctx *context.T, instanceDir string, p security.Principal, b security.Blessings) (security.Blessings, error) { + // Load the envelope for the instance, and get the publisher blessings in it + envelope, err := loadEnvelopeForInstance(ctx, instanceDir) + if err != nil { + return security.Blessings{}, err + } + + // Extend the device manager blessing with each publisher blessing provided + dmPrincipal := v23.GetPrincipal(ctx) + dmBlessings, _ := dmPrincipal.BlessingStore().Default() + + blessings, _ := publisherBlessingNames(ctx, *envelope) + for _, s := range blessings { + ctx.VI(2).Infof("adding publisher blessing %v for app %v", s, envelope.Title) + tmpBlessing, err := dmPrincipal.Bless(p.PublicKey(), dmBlessings, "a"+security.ChainSeparator+s, security.UnconstrainedUse()) + if err != nil { + return b, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Bless failed: %v", err)) + } + if b, err = security.UnionOfBlessings(b, tmpBlessing); err != nil { + return b, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("UnionOfBlessings failed: %v %v", b, tmpBlessing)) + } + } + + return b, nil +} + +// installationDir returns the path to the directory containing the app +// installation referred to by the invoker's suffix. Returns an error if the +// suffix does not name an installation or if the named installation does not +// exist. +func (i *appService) installationDir() (string, error) { + return installationDirCore(i.suffix, i.config.Root) +} + +// installPackages installs all the packages for a new version. +func installPackages(ctx *context.T, installationDir, versionDir string) error { + overridePackages, err := loadPackages(ctx, installationDir) + if err != nil { + return err + } + envelope, err := loadEnvelope(ctx, versionDir) + if err != nil { + return err + } + for pkg, _ := range overridePackages { + delete(envelope.Packages, pkg) + } + packagesDir := filepath.Join(versionDir, "packages") + if err := os.MkdirAll(packagesDir, os.FileMode(0755)); err != nil { + return err + } + installFrom := func(pkgs application.Packages, sourceDir string) error { + for pkg, _ := range pkgs { + pkgFile := filepath.Join(sourceDir, "pkg", pkg) + dst := filepath.Join(packagesDir, pkg) + if err := packages.Install(pkgFile, dst); err != nil { + return err + } + } + return nil + } + if err := installFrom(envelope.Packages, versionDir); err != nil { + return err + } + return installFrom(overridePackages, installationDir) +} + +// initializeSubAccessLists updates the provided perms for instance-specific +// Permissions. +func (i *appService) initializeSubAccessLists(instanceDir string, blessings []string, perms access.Permissions) error { + for _, b := range blessings { + b = b + string(security.ChainSeparator) + string(security.NoExtension) + for _, tag := range access.AllTypicalTags() { + perms.Add(security.BlessingPattern(b), string(tag)) + } + } + permsDir := path.Join(instanceDir, "acls") + return i.permsStore.Set(permsDir, perms, "") +} + +func (i *appService) newInstance(ctx *context.T, call device.ApplicationInstantiateServerCall) (string, string, error) { + installationDir, err := i.installationDir() + if err != nil { + return "", "", err + } + if !installationStateIs(installationDir, device.InstallationStateActive) { + return "", "", verror.New(errors.ErrInvalidOperation, ctx) + } + instanceID := generateID() + instanceDir := filepath.Join(installationDir, "instances", instanceDirName(instanceID)) + // Set permissions for app to have access. + if mkdirPerm(ctx, instanceDir, 0711) != nil { + return "", "", verror.New(errors.ErrOperationFailed, ctx) + } + rootDir := filepath.Join(instanceDir, "root") + if err := mkdir(ctx, rootDir); err != nil { + return instanceDir, instanceID, verror.New(errors.ErrOperationFailed, ctx, err) + } + installationLink := filepath.Join(instanceDir, "installation") + if err := os.Symlink(installationDir, installationLink); err != nil { + return instanceDir, instanceID, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Symlink(%v, %v) failed: %v", installationDir, installationLink, err)) + } + currLink := filepath.Join(installationDir, "current") + versionDir, err := filepath.EvalSymlinks(currLink) + if err != nil { + return instanceDir, instanceID, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", currLink, err)) + } + versionLink := filepath.Join(instanceDir, "version") + if err := os.Symlink(versionDir, versionLink); err != nil { + return instanceDir, instanceID, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Symlink(%v, %v) failed: %v", versionDir, versionLink, err)) + } + packagesDir, packagesLink := filepath.Join(versionLink, "packages"), filepath.Join(rootDir, "packages") + if err := os.Symlink(packagesDir, packagesLink); err != nil { + return instanceDir, instanceID, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Symlink(%v, %v) failed: %v", packagesDir, packagesLink, err)) + } + instanceInfo := new(instanceInfo) + if err := setupPrincipal(ctx, instanceDir, call, i.runner.principalMgr, instanceInfo, i.config.Root); err != nil { + return instanceDir, instanceID, err + } + if err := saveInstanceInfo(ctx, instanceDir, instanceInfo); err != nil { + return instanceDir, instanceID, err + } + blessings, _ := security.RemoteBlessingNames(ctx, call.Security()) + permsCopy := i.deviceAccessList.Copy() + if err := i.initializeSubAccessLists(instanceDir, blessings, permsCopy); err != nil { + return instanceDir, instanceID, err + } + if err := initializeInstance(instanceDir, device.InstanceStateNotRunning); err != nil { + return instanceDir, instanceID, err + } + // TODO(rjkroege): Divide the permission lists into those used by the device manager + // and those used by the application itself. + dmBlessings := security.LocalBlessingNames(ctx, call.Security()) + if err := setPermsForDebugging(dmBlessings, permsCopy, instanceDir, i.permsStore); err != nil { + return instanceDir, instanceID, err + } + return instanceDir, instanceID, nil +} + +func genCmd(ctx *context.T, instanceDir, nsRoot string) (*exec.Cmd, error) { + systemName, err := readSystemNameForInstance(instanceDir) + if err != nil { + return nil, err + } + + versionLink := filepath.Join(instanceDir, "version") + versionDir, err := filepath.EvalSymlinks(versionLink) + if err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", versionLink, err)) + } + envelope, err := loadEnvelope(ctx, versionDir) + if err != nil { + return nil, err + } + binPath := filepath.Join(versionDir, "bin") + if _, err := os.Stat(binPath); err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Stat(%v) failed: %v", binPath, err)) + } + + saArgs := suidAppCmdArgs{targetUser: systemName, binpath: binPath} + + // Pass the displayed name of the program (argv0 as seen in ps output) + // Envelope data comes from the user so we sanitize it for safety + _, relativeBinaryName := naming.SplitAddressName(envelope.Binary.File) + rawAppName := envelope.Title + "@" + relativeBinaryName + "/app" + sanitize := func(r rune) rune { + if strconv.IsPrint(r) { + return r + } else { + return '_' + } + } + appName := strings.Map(sanitize, rawAppName) + saArgs.progname = appName + + // Set the app's default namespace root to the local namespace. + saArgs.env = []string{ref.EnvNamespacePrefix + "=" + nsRoot} + saArgs.env = append(saArgs.env, envelope.Env...) + rootDir := filepath.Join(instanceDir, "root") + saArgs.dir = rootDir + saArgs.workspace = rootDir + + logDir := filepath.Join(instanceDir, "logs") + suidHelper.chownTree(ctx, suidHelper.getCurrentUser(), instanceDir, os.Stdout, os.Stdin) + if err := mkdirPerm(ctx, logDir, 0755); err != nil { + return nil, err + } + saArgs.logdir = logDir + timestamp := time.Now().UnixNano() + + stdoutLog := filepath.Join(logDir, fmt.Sprintf("STDOUT-%d", timestamp)) + if saArgs.stdout, err = openWriteFile(stdoutLog); err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("OpenFile(%v) failed: %v", stdoutLog, err)) + } + stderrLog := filepath.Join(logDir, fmt.Sprintf("STDERR-%d", timestamp)) + if saArgs.stderr, err = openWriteFile(stderrLog); err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("OpenFile(%v) failed: %v", stderrLog, err)) + } + + // Args to be passed by helper to the app. + appArgs := []string{"--log_dir=../logs"} + appArgs = append(appArgs, envelope.Args...) + + saArgs.appArgs = appArgs + return suidHelper.getAppCmd(ctx, &saArgs) +} + +// instanceNameFromDir returns the instance name, given the instanceDir. +func instanceNameFromDir(ctx *context.T, instanceDir string) (string, error) { + _, _, installation, instance := parseInstanceDir(instanceDir) + if installation == "" || instance == "" { + return "", fmt.Errorf("Unable to parse instanceDir %v", instanceDir) + } + + env, err := loadEnvelopeForInstance(ctx, instanceDir) + if err != nil { + return "", err + } + return env.Title + "/" + installation + "/" + instance, nil +} + +func (i *appRunner) startCmd(ctx *context.T, instanceDir string, cmd *exec.Cmd) (int, error) { + info, err := loadInstanceInfo(ctx, instanceDir) + if err != nil { + return 0, err + } + // Setup up the child process callback. + callbackState := i.callback + listener := callbackState.listenFor(ctx, mgmt.AppCycleManagerConfigKey) + defer listener.cleanup() + cfg := vexec.NewConfig() + installationLink := filepath.Join(instanceDir, "installation") + installationDir, err := filepath.EvalSymlinks(installationLink) + if err != nil { + return 0, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", installationLink, err)) + } + config, err := loadConfig(ctx, installationDir) + if err != nil { + return 0, err + } + for k, v := range config { + cfg.Set(k, v) + } + publisherBlessingsPrefix, _ := v23.GetPrincipal(ctx).BlessingStore().Default() + cfg.Set(mgmt.ParentNameConfigKey, listener.name()) + cfg.Set(mgmt.ProtocolConfigKey, "tcp") + cfg.Set(mgmt.AddressConfigKey, "127.0.0.1:0") + cfg.Set(mgmt.PublisherBlessingPrefixesKey, publisherBlessingsPrefix.String()) + if len(info.AppCycleBlessings) == 0 { + return 0, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("info.AppCycleBessings is missing")) + } + cfg.Set(mgmt.AppCycleBlessingsKey, info.AppCycleBlessings) + + if instanceName, err := instanceNameFromDir(ctx, instanceDir); err != nil { + return 0, err + } else { + cfg.Set(mgmt.InstanceNameKey, naming.Join(i.appServiceName, instanceName)) + } + + appPermsDir := filepath.Join(instanceDir, "debugacls", "data") + cfg.Set("v23.permissions.file", "runtime:"+appPermsDir) + + // This adds to cmd.Extrafiles. The helper expects a fixed fd, so this call needs + // to go before anything that conditionally adds to Extrafiles, like the agent + // setup code immediately below. + var handshaker appHandshaker + if err := handshaker.prepareToStart(ctx, cmd); err != nil { + return 0, err + } + defer handshaker.cleanup() + + if err := i.principalMgr.Serve(instanceDir, cfg); err != nil { + return 0, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Serve(%v) failed: %v", instanceDir, err)) + } + stopServing := true + defer func() { + if !stopServing { + return + } + if err := i.principalMgr.StopServing(instanceDir); err != nil { + ctx.Errorf("StopServing failed: %v", err) + } + }() + env, err := vexec.WriteConfigToEnv(cfg, cmd.Env) + if err != nil { + return 0, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("encoding config failed %v", err)) + } + cmd.Env = env + + // Start the child process. + if startErr := cmd.Start(); startErr != nil { + return 0, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Start() failed: %v", err)) + } + + // Wait for the suidhelper to exit. This is blocking as we assume the + // helper won't get stuck. + if err := cmd.Wait(); err != nil { + return 0, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Wait() on suidhelper failed: %v", err)) + } + + defer ctx.FlushLog() + pid, childName, err := handshaker.doHandshake(ctx, cmd, listener) + + if err != nil { + return 0, err + } + + info.AppCycleMgrName, info.Pid = childName, pid + if err := saveInstanceInfo(ctx, instanceDir, info); err != nil { + return 0, err + } + stopServing = false + return pid, nil +} + +func (i *appRunner) run(ctx *context.T, instanceDir string) error { + if err := transitionInstance(instanceDir, device.InstanceStateNotRunning, device.InstanceStateLaunching); err != nil { + return err + } + var pid int + + cmd, err := genCmd(ctx, instanceDir, i.mtAddress) + if err == nil { + pid, err = i.startCmd(ctx, instanceDir, cmd) + } + // TODO(caprita): If startCmd fails, we never reach startWatching; this + // means that the restart policy never kicks in, and the app stays dead. + // We should allow the app to be considered for restart if startCmd + // fails after having successfully started the app process. + if err != nil { + transitionInstance(instanceDir, device.InstanceStateLaunching, device.InstanceStateNotRunning) + return err + } + if err := transitionInstance(instanceDir, device.InstanceStateLaunching, device.InstanceStateRunning); err != nil { + return err + } + i.reap.startWatching(instanceDir, pid) + return nil +} + +func synchronizedShouldRestart(ctx *context.T, instanceDir string) bool { + info, err := loadInstanceInfo(nil, instanceDir) + if err != nil { + ctx.Error(err) + return false + } + + envelope, err := loadEnvelopeForInstance(nil, instanceDir) + if err != nil { + ctx.Error(err) + return false + } + + shouldRestart := newBasicRestartPolicy().decide(envelope, info) + + if err := saveInstanceInfo(nil, instanceDir, info); err != nil { + ctx.Error(err) + return false + } + return shouldRestart +} + +// restartAppIfNecessary restarts an application if its daemon +// configuration indicates that it should be running but the reaping +// functionality has previously determined that it is not. +// TODO(rjkroege): This routine has a low-likelyhood race condition in +// which it fails to restart an application when the app crashes and the +// device manager then crashes between the reaper marking the app not +// running and the go routine invoking this function having a chance to +// complete. +func (i *appRunner) restartAppIfNecessary(ctx *context.T, instanceDir string) { + if err := transitionInstance(instanceDir, device.InstanceStateNotRunning, device.InstanceStateLaunching); err != nil { + ctx.Error(err) + return + } + shouldRestart := synchronizedShouldRestart(ctx, instanceDir) + + if err := transitionInstance(instanceDir, device.InstanceStateLaunching, device.InstanceStateNotRunning); err != nil { + ctx.Error(err) + return + } + + if !shouldRestart { + return + } + + if instanceName, err := instanceNameFromDir(ctx, instanceDir); err != nil { + ctx.Error(err) + i.stats.incrRestarts("unknown") + } else { + i.stats.incrRestarts(instanceName) + } + + if err := i.run(ctx, instanceDir); err != nil { + ctx.Error(err) + } +} + +func (i *appService) Instantiate(ctx *context.T, call device.ApplicationInstantiateServerCall) (string, error) { + helper := i.config.Helper + instanceDir, instanceID, err := i.newInstance(ctx, call) + if err != nil { + CleanupDir(ctx, instanceDir, helper) + return "", err + } + systemName := suidHelper.usernameForPrincipal(ctx, call.Security(), i.uat) + if err := saveSystemNameForInstance(instanceDir, systemName); err != nil { + CleanupDir(ctx, instanceDir, helper) + return "", err + } + return instanceID, nil +} + +// instanceDir returns the path to the directory containing the app instance +// referred to by the given suffix relative to the given root directory. +// TODO(gauthamt): Make sure we pass the context to instanceDir. +func instanceDir(root string, suffix []string) (string, error) { + if nComponents := len(suffix); nComponents != 3 { + return "", verror.New(errors.ErrInvalidSuffix, nil) + } + app, installation, instance := suffix[0], suffix[1], suffix[2] + instancesDir := filepath.Join(root, applicationDirName(app), installationDirName(installation), "instances") + instanceDir := filepath.Join(instancesDir, instanceDirName(instance)) + return instanceDir, nil +} + +// parseInstanceDir is a partial inverse of instanceDir. It cannot retrieve the app name, +// as that has been hashed so it returns an appDir instead. +func parseInstanceDir(dir string) (prefix, appDir, installation, instance string) { + dirRE := regexp.MustCompile("(/.*)(/" + appDirPrefix + "[^/]+)/" + installationPrefix + "([^/]+)/" + "instances/" + instancePrefix + "([^/]+)$") + matches := dirRE.FindStringSubmatch(dir) + if len(matches) < 5 { + return "", "", "", "" + } + return matches[1], matches[2], matches[3], matches[4] +} + +// instanceDir returns the path to the directory containing the app instance +// referred to by the invoker's suffix, as well as the corresponding not-running +// instance dir. Returns an error if the suffix does not name an instance. +func (i *appService) instanceDir() (string, error) { + return instanceDir(i.config.Root, i.suffix) +} + +func (i *appService) Run(ctx *context.T, call rpc.ServerCall) error { + instanceDir, err := i.instanceDir() + if err != nil { + return err + } + + systemName := suidHelper.usernameForPrincipal(ctx, call.Security(), i.uat) + startSystemName, err := readSystemNameForInstance(instanceDir) + if err != nil { + return err + } + + if startSystemName != systemName { + return verror.New(verror.ErrNoAccess, ctx, "Not allowed to resume an application under a different system name.") + } + + i.stats.incrRuns(naming.Join(i.suffix...)) + + // TODO(caprita): We should reset the Restarts and RestartWindowBegan + // fields in the instance info when the instance is started with Run. + + return i.runner.run(ctx, instanceDir) +} + +func stopAppRemotely(ctx *context.T, appVON string, deadline time.Duration) error { + appStub := appcycle.AppCycleClient(appVON) + ctx, cancel := context.WithTimeout(ctx, deadline) + defer cancel() + stream, err := appStub.Stop(ctx) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("%v.Stop() failed: %v", appVON, err)) + } + rstream := stream.RecvStream() + for rstream.Advance() { + ctx.VI(2).Infof("%v.Stop() task update: %v", appVON, rstream.Value()) + } + if err := rstream.Err(); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Advance() failed: %v", err)) + } + if err := stream.Finish(); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Finish() failed: %v", err)) + } + return nil +} + +// stop attempts to stop the instance's process; returns true if successful, or +// false if the process is still running. +func (i *appService) stop(ctx *context.T, instanceDir string, info *instanceInfo, reap *reaper, deadline time.Duration) (bool, error) { + pid := info.Pid + // The reaper should stop tracking this instance, and, in particular, + // not attempt to restart it. + reap.stopWatching(instanceDir) + processExited, stopGoroutine := make(chan struct{}), make(chan struct{}) + defer close(stopGoroutine) + go func() { + for { + if !isAlive(ctx, pid) { + close(processExited) + return + } + select { + case <-stopGoroutine: + return + default: + } + time.Sleep(time.Millisecond) + } + }() + deadlineExpired := time.After(deadline) + err := stopAppRemotely(ctx, info.AppCycleMgrName, deadline) + select { + case <-processExited: + if err != nil { + err = verror.New(errStoppedWithErrors, ctx, fmt.Sprintf("process exited uncleanly upon remote stop: %v", err)) + } + return true, err + case <-deadlineExpired: + } + reap.forciblySuspend(instanceDir) + // Give it some grace period for the process to die after forceful + // shutdown. + gracePeriod := 5 * time.Second + deadlineExpired = time.After(gracePeriod) + select { + case <-processExited: + return true, verror.New(errStoppedWithErrors, ctx, fmt.Sprintf("process failed to exit cleanly upon remote stop (%v) and was forcefully terminated", err)) + case <-deadlineExpired: + // The process just won't die. We'll declare the stop operation + // unsuccessful and switch the instance back to running + // state. We let the reaper deal with it going forward + // (including restarting it if restarts are enabled). + reap.startWatching(instanceDir, pid) + return false, verror.New(errStopFailed, ctx, fmt.Sprintf("process failed to exit within %v after force stop", gracePeriod)) + } +} + +func (i *appService) Delete(ctx *context.T, _ rpc.ServerCall) error { + instanceDir, err := i.instanceDir() + if err != nil { + return err + } + return transitionInstance(instanceDir, device.InstanceStateNotRunning, device.InstanceStateDeleted) +} + +func (i *appService) Kill(ctx *context.T, _ rpc.ServerCall, deadline time.Duration) error { + instanceDir, err := i.instanceDir() + if err != nil { + return err + } + if err := transitionInstance(instanceDir, device.InstanceStateRunning, device.InstanceStateDying); err != nil { + return err + } + info, err := loadInstanceInfo(ctx, instanceDir) + if err != nil { + return err + } + if exited, err := i.stop(ctx, instanceDir, info, i.runner.reap, deadline); !exited { + // If the process failed to terminate, it's going back in state + // running (as if the Kill never happened). The client may try + // again. + if err := transitionInstance(instanceDir, device.InstanceStateDying, device.InstanceStateRunning); err != nil { + ctx.Errorf("transitionInstance(%v, %v, %v): %v", instanceDir, device.InstanceStateDying, device.InstanceStateRunning, err) + } + // Return the stop error. + return err + } else if err != nil { + ctx.Errorf("stop %v ultimately succeeded, but had encountered an error: %v", instanceDir, err) + } + // The app exited, so we can stop serving the principal. + if err := i.runner.principalMgr.StopServing(instanceDir); err != nil { + ctx.Errorf("StopServing(%v) failed: %v", instanceDir, err) + } + return transitionInstance(instanceDir, device.InstanceStateDying, device.InstanceStateNotRunning) +} + +func (i *appService) Uninstall(*context.T, rpc.ServerCall) error { + installationDir, err := i.installationDir() + if err != nil { + return err + } + return transitionInstallation(installationDir, device.InstallationStateActive, device.InstallationStateUninstalled) +} + +func updateInstance(ctx *context.T, instanceDir string) (err error) { + // Only not-running instances can be updated. + if err := transitionInstance(instanceDir, device.InstanceStateNotRunning, device.InstanceStateUpdating); err != nil { + return err + } + defer func() { + terr := transitionInstance(instanceDir, device.InstanceStateUpdating, device.InstanceStateNotRunning) + if err == nil { + err = terr + } + }() + // Check if a newer version of the installation is available. + versionLink := filepath.Join(instanceDir, "version") + versionDir, err := filepath.EvalSymlinks(versionLink) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", versionLink, err)) + } + latestVersionLink := filepath.Join(instanceDir, "installation", "current") + latestVersionDir, err := filepath.EvalSymlinks(latestVersionLink) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", latestVersionLink, err)) + } + if versionDir == latestVersionDir { + return verror.New(errors.ErrUpdateNoOp, ctx) + } + // Update to the newer version. Note, this is the only mutation + // performed to the instance, and, since it's atomic, the state of the + // instance is consistent at all times. + return UpdateLink(latestVersionDir, versionLink) +} + +func updateInstallation(ctx *context.T, installationDir string) error { + if !installationStateIs(installationDir, device.InstallationStateActive) { + return verror.New(errors.ErrInvalidOperation, ctx) + } + originVON, err := loadOrigin(ctx, installationDir) + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(ctx, rpcContextLongTimeout) + defer cancel() + newEnvelope, err := fetchAppEnvelope(ctx, originVON) + if err != nil { + return err + } + currLink := filepath.Join(installationDir, "current") + oldVersionDir, err := filepath.EvalSymlinks(currLink) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", currLink, err)) + } + // NOTE(caprita): A race can occur between two competing updates, where + // both use the old version as their baseline. This can result in both + // updates succeeding even if they are updating the app installation to + // the same new envelope. This will result in one of the updates + // becoming the new 'current'. Both versions will point their + // 'previous' link to the old version. This doesn't appear to be of + // practical concern, so we avoid the complexity of synchronizing + // updates. + oldEnvelope, err := loadEnvelope(ctx, oldVersionDir) + if err != nil { + return err + } + if oldEnvelope.Title != newEnvelope.Title { + return verror.New(errors.ErrAppTitleMismatch, ctx) + } + if reflect.DeepEqual(oldEnvelope, newEnvelope) { + return verror.New(errors.ErrUpdateNoOp, ctx) + } + versionDir, err := newVersion(ctx, installationDir, newEnvelope, oldVersionDir) + if err != nil { + CleanupDir(ctx, versionDir, "") + return err + } + return nil +} + +func (i *appService) Update(ctx *context.T, _ rpc.ServerCall) error { + if installationDir, err := i.installationDir(); err == nil { + return updateInstallation(ctx, installationDir) + } + if instanceDir, err := i.instanceDir(); err == nil { + return updateInstance(ctx, instanceDir) + } + return verror.New(errors.ErrInvalidSuffix, nil) +} + +func (*appService) UpdateTo(_ *context.T, _ rpc.ServerCall, von string) error { + // TODO(jsimsa): Implement. + return nil +} + +func revertInstance(ctx *context.T, instanceDir string) (err error) { + // Only not-running instances can be reverted. + if err := transitionInstance(instanceDir, device.InstanceStateNotRunning, device.InstanceStateUpdating); err != nil { + return err + } + defer func() { + terr := transitionInstance(instanceDir, device.InstanceStateUpdating, device.InstanceStateNotRunning) + if err == nil { + err = terr + } + }() + versionLink := filepath.Join(instanceDir, "version") + versionDir, err := filepath.EvalSymlinks(versionLink) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", versionLink, err)) + } + previousLink := filepath.Join(versionDir, "previous") + if _, err := os.Lstat(previousLink); err != nil { + if os.IsNotExist(err) { + // No 'previous' link -- must be the first version. + return verror.New(errors.ErrUpdateNoOp, ctx) + } + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Lstat(%v) failed: %v", previousLink, err)) + } + prevVersionDir, err := filepath.EvalSymlinks(previousLink) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", previousLink, err)) + } + return UpdateLink(prevVersionDir, versionLink) +} + +func revertInstallation(ctx *context.T, installationDir string) error { + if !installationStateIs(installationDir, device.InstallationStateActive) { + return verror.New(errors.ErrInvalidOperation, ctx) + } + // NOTE(caprita): A race can occur between an update and a revert, where + // both use the same current version as their starting point. This will + // render the update inconsequential. This doesn't appear to be of + // practical concern, so we avoid the complexity of synchronizing + // updates and revert operations. + currLink := filepath.Join(installationDir, "current") + currVersionDir, err := filepath.EvalSymlinks(currLink) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", currLink, err)) + } + previousLink := filepath.Join(currVersionDir, "previous") + if _, err := os.Lstat(previousLink); err != nil { + if os.IsNotExist(err) { + // No 'previous' link -- must be the first version. + return verror.New(errors.ErrUpdateNoOp, ctx) + } + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Lstat(%v) failed: %v", previousLink, err)) + } + prevVersionDir, err := filepath.EvalSymlinks(previousLink) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", previousLink, err)) + } + return UpdateLink(prevVersionDir, currLink) +} + +func (i *appService) Revert(ctx *context.T, _ rpc.ServerCall) error { + if installationDir, err := i.installationDir(); err == nil { + return revertInstallation(ctx, installationDir) + } + if instanceDir, err := i.instanceDir(); err == nil { + return revertInstance(ctx, instanceDir) + } + return verror.New(errors.ErrInvalidSuffix, nil) +} + +type treeNode struct { + children map[string]*treeNode +} + +func newTreeNode() *treeNode { + return &treeNode{children: make(map[string]*treeNode)} +} + +func (n *treeNode) find(names []string, create bool) *treeNode { + for { + if len(names) == 0 { + return n + } + if next, ok := n.children[names[0]]; ok { + n = next + names = names[1:] + continue + } + if create { + nn := newTreeNode() + n.children[names[0]] = nn + n = nn + names = names[1:] + continue + } + return nil + } +} + +func (i *appService) scanEnvelopes(ctx *context.T, tree *treeNode, appDir string) { + // Find all envelopes, extract installID. + envGlob := []string{i.config.Root, appDir, installationPrefix + "*", "*", "envelope"} + envelopes, err := filepath.Glob(filepath.Join(envGlob...)) + if err != nil { + ctx.Errorf("unexpected error: %v", err) + return + } + for _, path := range envelopes { + env, err := loadEnvelope(ctx, filepath.Dir(path)) + if err != nil { + continue + } + relpath, _ := filepath.Rel(i.config.Root, path) + elems := strings.Split(relpath, string(filepath.Separator)) + if len(elems) != len(envGlob)-1 { + ctx.Errorf("unexpected number of path components: %q (%q)", elems, path) + continue + } + installID := strings.TrimPrefix(elems[1], installationPrefix) + tree.find([]string{env.Title, installID}, true) + } + return +} + +func (i *appService) scanInstances(ctx *context.T, tree *treeNode) { + if len(i.suffix) < 2 { + return + } + title := i.suffix[0] + installDir, err := installationDirCore(i.suffix[:2], i.config.Root) + if err != nil { + return + } + // Add the node corresponding to the installation itself. + tree.find(i.suffix[:2], true) + // Find all instances. + infoGlob := []string{installDir, "instances", instancePrefix + "*", "info"} + instances, err := filepath.Glob(filepath.Join(infoGlob...)) + if err != nil { + ctx.Errorf("unexpected error: %v", err) + return + } + for _, path := range instances { + instanceDir := filepath.Dir(path) + i.scanInstance(ctx, tree, title, instanceDir) + } + return +} + +func (i *appService) scanInstance(ctx *context.T, tree *treeNode, title, instanceDir string) { + if _, err := loadInstanceInfo(ctx, instanceDir); err != nil { + return + } + rootDir, _, installID, instanceID := parseInstanceDir(instanceDir) + if installID == "" || instanceID == "" || filepath.Clean(i.config.Root) != filepath.Clean(rootDir) { + ctx.Errorf("failed to parse instanceDir %v (got: %v %v %v)", instanceDir, rootDir, installID, instanceID) + return + } + + tree.find([]string{title, installID, instanceID, "logs"}, true) + if instanceStateIs(instanceDir, device.InstanceStateRunning) { + for _, obj := range []string{"pprof", "stats"} { + tree.find([]string{title, installID, instanceID, obj}, true) + } + } +} + +func (i *appService) GlobChildren__(ctx *context.T, call rpc.GlobChildrenServerCall, m *glob.Element) error { + tree := newTreeNode() + switch len(i.suffix) { + case 0: + i.scanEnvelopes(ctx, tree, appDirPrefix+"*") + case 1: + appDir := applicationDirName(i.suffix[0]) + i.scanEnvelopes(ctx, tree, appDir) + case 2: + i.scanInstances(ctx, tree) + case 3: + dir, err := i.instanceDir() + if err != nil { + break + } + i.scanInstance(ctx, tree, i.suffix[0], dir) + default: + return verror.New(verror.ErrNoExist, nil, i.suffix) + } + n := tree.find(i.suffix, false) + if n == nil { + return verror.New(errors.ErrInvalidSuffix, nil) + } + for child, _ := range n.children { + if m.Match(child) { + call.SendStream().Send(naming.GlobChildrenReplyName{Value: child}) + } + } + return nil +} + +// TODO(rjkroege): Refactor to eliminate redundancy with newAppSpecificAuthorizer. +func dirFromSuffix(ctx *context.T, suffix []string, root string) (string, bool, error) { + if len(suffix) == 2 { + p, err := installationDirCore(suffix, root) + if err != nil { + ctx.Errorf("dirFromSuffix failed: %v", err) + return "", false, err + } + return p, false, nil + } else if len(suffix) > 2 { + p, err := instanceDir(root, suffix[0:3]) + if err != nil { + ctx.Errorf("dirFromSuffix failed: %v", err) + return "", false, err + } + return p, true, nil + } + return "", false, verror.New(errors.ErrInvalidSuffix, nil) +} + +// TODO(rjkroege): Consider maintaining an in-memory Permissions cache. +func (i *appService) SetPermissions(ctx *context.T, call rpc.ServerCall, perms access.Permissions, version string) error { + dir, isInstance, err := dirFromSuffix(ctx, i.suffix, i.config.Root) + if err != nil { + return err + } + if isInstance { + dmBlessings := security.LocalBlessingNames(ctx, call.Security()) + if err := setPermsForDebugging(dmBlessings, perms, dir, i.permsStore); err != nil { + return err + } + } + return i.permsStore.Set(path.Join(dir, "acls"), perms, version) +} + +func (i *appService) GetPermissions(ctx *context.T, _ rpc.ServerCall) (perms access.Permissions, version string, err error) { + dir, _, err := dirFromSuffix(ctx, i.suffix, i.config.Root) + if err != nil { + return nil, "", err + } + return i.permsStore.Get(path.Join(dir, "acls")) +} + +func (i *appService) Debug(ctx *context.T, call rpc.ServerCall) (string, error) { + switch len(i.suffix) { + case 2: + return i.installationDebug(ctx) + case 3: + return i.instanceDebug(ctx, call.Security()) + default: + return "", verror.New(errors.ErrInvalidSuffix, nil) + } +} + +func (i *appService) installationDebug(ctx *context.T) (string, error) { + const installationDebug = `Installation dir: {{.InstallationDir}} + +Origin: {{.Origin}} + +Envelope: {{printf "%+v" .Envelope}} + +Config: {{printf "%+v" .Config}} +` + installationDebugTemplate, err := template.New("installation-debug").Parse(installationDebug) + if err != nil { + return "", err + } + + installationDir, err := i.installationDir() + if err != nil { + return "", err + } + debugInfo := struct { + InstallationDir, Origin string + Envelope *application.Envelope + Config device.Config + }{} + debugInfo.InstallationDir = installationDir + + if origin, err := loadOrigin(ctx, installationDir); err != nil { + return "", err + } else { + debugInfo.Origin = origin + } + + currLink := filepath.Join(installationDir, "current") + if envelope, err := loadEnvelope(ctx, currLink); err != nil { + return "", err + } else { + debugInfo.Envelope = envelope + } + + if config, err := loadConfig(ctx, installationDir); err != nil { + return "", err + } else { + debugInfo.Config = config + } + + var buf bytes.Buffer + if err := installationDebugTemplate.Execute(&buf, debugInfo); err != nil { + return "", err + } + return buf.String(), nil + +} + +func (i *appService) instanceDebug(ctx *context.T, call security.Call) (string, error) { + const instanceDebug = `Instance dir: {{.InstanceDir}} + +System name / start system name: {{.SystemName}} / {{.StartSystemName}} + +Cmd: {{printf "%+v" .Cmd}} + +Envelope: {{printf "%+v" .Envelope}} + +Info: {{printf "%+v" .Info}} + +Principal: {{.PrincipalDebug}} +Public Key: {{.Principal.PublicKey}} +Blessing Store: {{.Principal.BlessingStore.DebugString}} +Roots: {{.Principal.Roots.DebugString}} +` + instanceDebugTemplate, err := template.New("instance-debug").Parse(instanceDebug) + if err != nil { + return "", err + } + + instanceDir, err := i.instanceDir() + if err != nil { + return "", err + } + debugInfo := struct { + InstanceDir, SystemName, StartSystemName string + Cmd *exec.Cmd + Envelope *application.Envelope + Info *instanceInfo + Principal agent.Principal + PrincipalDebug string + }{} + debugInfo.InstanceDir = instanceDir + + debugInfo.SystemName = suidHelper.usernameForPrincipal(ctx, call, i.uat) + if startSystemName, err := readSystemNameForInstance(instanceDir); err != nil { + return "", err + } else { + debugInfo.StartSystemName = startSystemName + } + + if info, err := loadInstanceInfo(ctx, instanceDir); err != nil { + return "", err + } else { + debugInfo.Info = info + } + if cmd, err := genCmd(ctx, instanceDir, i.runner.mtAddress); err != nil { + return "", err + } else { + debugInfo.Cmd = cmd + } + + if envelope, err := loadEnvelopeForInstance(ctx, instanceDir); err != nil { + return "", err + } else { + debugInfo.Envelope = envelope + } + // TODO(caprita): Load requires that the principal be Serve-ing. + if debugInfo.Principal, err = i.runner.principalMgr.Load(instanceDir); err != nil { + return "", err + } + defer debugInfo.Principal.Close() + debugInfo.PrincipalDebug = i.runner.principalMgr.Debug(instanceDir) + var buf bytes.Buffer + if err := instanceDebugTemplate.Execute(&buf, debugInfo); err != nil { + return "", err + } + return buf.String(), nil +} + +func (i *appService) Status(ctx *context.T, _ rpc.ServerCall) (device.Status, error) { + switch len(i.suffix) { + case 2: + status, err := i.installationStatus(ctx) + return device.StatusInstallation{Value: status}, err + case 3: + status, err := i.instanceStatus(ctx) + return device.StatusInstance{Value: status}, err + default: + return nil, verror.New(errors.ErrInvalidSuffix, ctx) + } +} + +func (i *appService) installationStatus(ctx *context.T) (device.InstallationStatus, error) { + installationDir, err := i.installationDir() + if err != nil { + return device.InstallationStatus{}, err + } + state, err := getInstallationState(installationDir) + if err != nil { + return device.InstallationStatus{}, err + } + versionLink := filepath.Join(installationDir, "current") + versionDir, err := filepath.EvalSymlinks(versionLink) + if err != nil { + return device.InstallationStatus{}, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", versionLink, err)) + } + return device.InstallationStatus{ + State: state, + Version: filepath.Base(versionDir), + }, nil +} + +func (i *appService) instanceStatus(ctx *context.T) (device.InstanceStatus, error) { + instanceDir, err := i.instanceDir() + if err != nil { + return device.InstanceStatus{}, err + } + state, err := getInstanceState(instanceDir) + if err != nil { + return device.InstanceStatus{}, err + } + versionLink := filepath.Join(instanceDir, "version") + versionDir, err := filepath.EvalSymlinks(versionLink) + if err != nil { + return device.InstanceStatus{}, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("EvalSymlinks(%v) failed: %v", versionLink, err)) + } + return device.InstanceStatus{ + State: state, + Version: filepath.Base(versionDir), + }, nil +} + +func createCallbackBlessings(ctx *context.T, app security.PublicKey) (string, error) { + dm := v23.GetPrincipal(ctx) // device manager principal + dmB, _ := dm.BlessingStore().Default() + // NOTE(caprita/ataly): Giving the app an unconstrained blessing from + // the device manager's default presents the app with a blessing that's + // potentially more powerful than what is strictly needed to allow + // communication between device manager and app (which could be more + // narrowly accomplished by using a custom-purpose self-signed blessing + // that the device manger produces solely to talk to the app). + b, err := dm.Bless(app, dmB, "callback", security.UnconstrainedUse()) + if err != nil { + return "", verror.New(errors.ErrOperationFailed, ctx, err) + } + bytes, err := vom.Encode(b) + if err != nil { + return "", verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("failed to encode app cycle blessings: %v", err)) + } + return base64.URLEncoding.EncodeToString(bytes), nil +} diff --git a/x/ref/services/device/deviced/internal/impl/app_starting_util.go b/x/ref/services/device/deviced/internal/impl/app_starting_util.go new file mode 100644 index 000000000..fc5ff035d --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/app_starting_util.go @@ -0,0 +1,144 @@ +// Copyright 2015 The Vanadium 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 impl + +// TODO -- Ideally the code in this file would be integrated with the instance reaping, +// so we avoid having two process polling loops. This code is currently separate because +// the actions taken when the app dies (or is caught lying about its pid) prior to being +// considered running are fairly different from what's currently done by the reaper in +// handling deaths that occur after the app started successfully. + +import ( + "encoding/binary" + "fmt" + "os" + "os/exec" + "syscall" + "time" + + "v.io/v23/context" + "v.io/v23/verror" + "v.io/x/ref/services/device/internal/errors" + "v.io/x/ref/services/device/internal/suid" +) + +// appWatcher watches the pid of a running app until either the pid exits or stop() +// is called +type appWatcher struct { + pid int // Pid to watch + callback func() // Called if the pid exits or if stop() is invoked + stopper chan struct{} // Used to stop the appWatcher +} + +func newAppWatcher(pidToWatch int, callOnPidExit func()) *appWatcher { + return &appWatcher{ + pid: pidToWatch, + callback: callOnPidExit, + stopper: make(chan struct{}, 1), + } +} + +func (a *appWatcher) stop() { + close(a.stopper) +} + +func (a *appWatcher) watchAppPid(ctx *context.T) { + defer a.callback() + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if err := syscall.Kill(a.pid, 0); err != nil && err != syscall.EPERM { + ctx.Errorf("App died in startup: pid=%d: %v", a.pid, err) + return + } else { + ctx.VI(2).Infof("App pid %d is alive", a.pid) + } + + case <-a.stopper: + ctx.Errorf("AppWatcher was stopped") + return + } + } + // Not reached. +} + +// appHandshaker is a utility to do the app handshake for a newly started app while +// reacting quickly if the app crashes. appHandshaker reads two pids from the app (one +// from the helper that forked the app, and the other from the app itself). If the app +// appears to be lying about its own pid, it will kill the app. +type appHandshaker struct { + helperRead, helperWrite *os.File +} + +func (a *appHandshaker) cleanup() { + if a.helperRead != nil { + a.helperRead.Close() + a.helperRead = nil + } + if a.helperWrite != nil { + a.helperWrite.Close() + a.helperWrite = nil + } +} + +// prepareToStart sets up the pipe used to talk to the helper. It must be called before +// the app is started so that the app will inherit the file descriptor +func (a *appHandshaker) prepareToStart(ctx *context.T, cmd *exec.Cmd) error { + if suid.PipeToParentFD != len(cmd.ExtraFiles)+3 { + return verror.New(errors.ErrOperationFailed, ctx, + fmt.Sprintf("FD expected by helper (%v) was not available (%v)", + suid.PipeToParentFD, len(cmd.ExtraFiles))) + } + var err error + a.helperRead, a.helperWrite, err = os.Pipe() + if err != nil { + ctx.Errorf("Failed to create pipe: %v", err) + return err + } + cmd.ExtraFiles = append(cmd.ExtraFiles, a.helperWrite) + return nil +} + +// doAppHandshake executes the startup handshake for the app. Upon success, it returns the +// pid and appCycle manager name for the started app. +// +// cmd should have been set up to use a helper for the app and cmd.Start() +// and cmd.Wait() should already have been called (so we know the helper is done) +func (a *appHandshaker) doHandshake(ctx *context.T, cmd *exec.Cmd, listener callbackListener) (int, string, error) { + // Close our copy of helperWrite to make helperRead return EOF once the + // helper's copy of helperWrite is closed. + a.helperWrite.Close() + a.helperWrite = nil + + // Get the app pid from the helper. This won't block as the helper is done + var pid32 int32 + if err := binary.Read(a.helperRead, binary.LittleEndian, &pid32); err != nil { + ctx.Errorf("Error reading app pid from child: %v", err) + return 0, "", verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("failed to read pid from helper: %v", err)) + } + pidFromHelper := int(pid32) + ctx.Infof("Read app pid %v from child", pidFromHelper) + + // Watch the app pid in case it exits. + watcher := newAppWatcher(pidFromHelper, func() { + listener.stop() + }) + go watcher.watchAppPid(ctx) + defer watcher.stop() + + // The appWatcher will stop the listener if the pid dies while waiting below + childName, err := listener.waitForValue(childReadyTimeout) + if err != nil { + suidHelper.terminatePid(ctx, pidFromHelper, nil, nil) + return 0, "", verror.New(errors.ErrOperationFailed, ctx, + fmt.Sprintf("Waiting for child name: %v", err)) + } + + return pidFromHelper, childName, nil +} diff --git a/x/ref/services/device/deviced/internal/impl/app_state.go b/x/ref/services/device/deviced/internal/impl/app_state.go new file mode 100644 index 000000000..98936d2ad --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/app_state.go @@ -0,0 +1,91 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "v.io/v23/services/device" + "v.io/v23/verror" + "v.io/x/ref/services/device/internal/errors" +) + +func getInstallationState(installationDir string) (device.InstallationState, error) { + for i := 0; i < 2; i++ { + // TODO(caprita): This is racy w.r.t. instances that are + // transitioning states. We currently do a retry because of + // this, which in practice should be sufficient; a more + // deterministic solution would involve changing the way we + // store status. + for _, s := range device.InstallationStateAll { + if installationStateIs(installationDir, s) { + return s, nil + } + } + } + return device.InstallationStateActive, verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("failed to determine state for installation in dir %v", installationDir)) +} + +func installationStateIs(installationDir string, state device.InstallationState) bool { + if _, err := os.Stat(filepath.Join(installationDir, state.String())); err != nil { + return false + } + return true +} + +func transitionInstallation(installationDir string, initial, target device.InstallationState) error { + return transitionState(installationDir, initial, target) +} + +func initializeInstallation(installationDir string, initial device.InstallationState) error { + return initializeState(installationDir, initial) +} + +func getInstanceState(instanceDir string) (device.InstanceState, error) { + for _, s := range device.InstanceStateAll { + if instanceStateIs(instanceDir, s) { + return s, nil + } + } + return device.InstanceStateLaunching, verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("failed to determine state for instance in dir %v", instanceDir)) +} + +func instanceStateIs(instanceDir string, state device.InstanceState) bool { + if _, err := os.Stat(filepath.Join(instanceDir, state.String())); err != nil { + return false + } + return true +} + +func transitionInstance(instanceDir string, initial, target device.InstanceState) error { + return transitionState(instanceDir, initial, target) +} + +func initializeInstance(instanceDir string, initial device.InstanceState) error { + return initializeState(instanceDir, initial) +} + +func transitionState(dir string, initial, target fmt.Stringer) error { + initialState := filepath.Join(dir, initial.String()) + targetState := filepath.Join(dir, target.String()) + if err := os.Rename(initialState, targetState); err != nil { + if os.IsNotExist(err) { + return verror.New(errors.ErrInvalidOperation, nil, err) + } + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("Rename(%v, %v) failed: %v", initialState, targetState, err)) + } + return nil +} + +func initializeState(dir string, initial fmt.Stringer) error { + initialStatus := filepath.Join(dir, initial.String()) + if err := ioutil.WriteFile(initialStatus, []byte("status"), 0600); err != nil { + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("WriteFile(%v) failed: %v", initialStatus, err)) + } + return nil +} diff --git a/x/ref/services/device/deviced/internal/impl/app_state_test.go b/x/ref/services/device/deviced/internal/impl/app_state_test.go new file mode 100644 index 000000000..614c6470c --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/app_state_test.go @@ -0,0 +1,97 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "io/ioutil" + "os" + "testing" + + "v.io/v23/services/device" +) + +// TestInstallationState verifies the state transition logic for app installations. +func TestInstallationState(t *testing.T) { + dir, err := ioutil.TempDir("", "installation") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(dir) + // Uninitialized state. + if transitionInstallation(dir, device.InstallationStateActive, device.InstallationStateUninstalled) == nil { + t.Fatalf("transitionInstallation should have failed") + } + if s, err := getInstallationState(dir); err == nil { + t.Fatalf("getInstallationState should have failed, got state %v instead", s) + } + if isActive, isUninstalled := installationStateIs(dir, device.InstallationStateActive), installationStateIs(dir, device.InstallationStateUninstalled); isActive || isUninstalled { + t.Fatalf("isActive, isUninstalled = %t, %t (expected false, false)", isActive, isUninstalled) + } + // Initialize. + if err := initializeInstallation(dir, device.InstallationStateActive); err != nil { + t.Fatalf("initializeInstallation failed: %v", err) + } + if !installationStateIs(dir, device.InstallationStateActive) { + t.Fatalf("Installation state expected to be %v", device.InstallationStateActive) + } + if s, err := getInstallationState(dir); s != device.InstallationStateActive || err != nil { + t.Fatalf("getInstallationState expected (%v, %v), got (%v, %v) instead", device.InstallationStateActive, nil, s, err) + } + if err := transitionInstallation(dir, device.InstallationStateActive, device.InstallationStateUninstalled); err != nil { + t.Fatalf("transitionInstallation failed: %v", err) + } + if !installationStateIs(dir, device.InstallationStateUninstalled) { + t.Fatalf("Installation state expected to be %v", device.InstallationStateUninstalled) + } + if s, err := getInstallationState(dir); s != device.InstallationStateUninstalled || err != nil { + t.Fatalf("getInstallationState expected (%v, %v), got (%v, %v) instead", device.InstallationStateUninstalled, nil, s, err) + } + // Invalid transition: wrong initial state. + if transitionInstallation(dir, device.InstallationStateActive, device.InstallationStateUninstalled) == nil { + t.Fatalf("transitionInstallation should have failed") + } + if !installationStateIs(dir, device.InstallationStateUninstalled) { + t.Fatalf("Installation state expected to be %v", device.InstallationStateUninstalled) + } +} + +// TestInstanceState verifies the state transition logic for app instances. +func TestInstanceState(t *testing.T) { + dir, err := ioutil.TempDir("", "instance") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(dir) + // Uninitialized state. + if transitionInstance(dir, device.InstanceStateLaunching, device.InstanceStateRunning) == nil { + t.Fatalf("transitionInstance should have failed") + } + if s, err := getInstanceState(dir); err == nil { + t.Fatalf("getInstanceState should have failed, got state %v instead", s) + } + // Initialize. + if err := initializeInstance(dir, device.InstanceStateDying); err != nil { + t.Fatalf("initializeInstance failed: %v", err) + } + if s, err := getInstanceState(dir); s != device.InstanceStateDying || err != nil { + t.Fatalf("getInstanceState expected (%v, %v), got (%v, %v) instead", device.InstanceStateDying, nil, s, err) + } + if err := transitionInstance(dir, device.InstanceStateDying, device.InstanceStateNotRunning); err != nil { + t.Fatalf("transitionInstance failed: %v", err) + } + if s, err := getInstanceState(dir); s != device.InstanceStateNotRunning || err != nil { + t.Fatalf("getInstanceState expected (%v, %v), got (%v, %v) instead", device.InstanceStateNotRunning, nil, s, err) + } + // Invalid transition: wrong initial state. + if transitionInstance(dir, device.InstanceStateDying, device.InstanceStateNotRunning) == nil { + t.Fatalf("transitionInstance should have failed") + } + if err := transitionInstance(dir, device.InstanceStateNotRunning, device.InstanceStateDeleted); err != nil { + t.Fatalf("transitionInstance failed: %v", err) + } + if s, err := getInstanceState(dir); s != device.InstanceStateDeleted || err != nil { + t.Fatalf("getInstanceState expected (%v, %v), got (%v, %v) instead", device.InstanceStateDeleted, nil, s, err) + } +} diff --git a/x/ref/services/device/deviced/internal/impl/applife/app_life_test.go b/x/ref/services/device/deviced/internal/impl/applife/app_life_test.go new file mode 100644 index 000000000..0e545358f --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/applife/app_life_test.go @@ -0,0 +1,667 @@ +// Copyright 2015 The Vanadium 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 applife_test + +import ( + "crypto/md5" + "encoding/base64" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + "syscall" + "testing" + "time" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/security" + "v.io/v23/services/application" + "v.io/v23/services/device" + "v.io/x/ref" + "v.io/x/ref/lib/mgmt" + vsecurity "v.io/x/ref/lib/security" + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" + "v.io/x/ref/services/device/deviced/internal/versioning" + "v.io/x/ref/services/device/internal/errors" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" +) + +func instanceDirForApp(root, appID, instanceID string) string { + applicationDirName := func(title string) string { + h := md5.New() + h.Write([]byte(title)) + hash := strings.TrimRight(base64.URLEncoding.EncodeToString(h.Sum(nil)), "=") + return "app-" + hash + } + components := strings.Split(appID, "/") + appTitle, installationID := components[0], components[1] + return filepath.Join(root, applicationDirName(appTitle), "installation-"+installationID, "instances", "instance-"+instanceID) +} + +func verifyAppWorkspace(t *testing.T, root, appID, instanceID string) { + // HACK ALERT: for now, we peek inside the device manager's directory + // structure (which ought to be opaque) to check for what the app has + // written to its local root. + // + // TODO(caprita): add support to device manager to browse logs/app local + // root. + rootDir := filepath.Join(instanceDirForApp(root, appID, instanceID), "root") + testFile := filepath.Join(rootDir, "testfile") + if read, err := ioutil.ReadFile(testFile); err != nil { + t.Fatalf("Failed to read %v: %v", testFile, err) + } else if want, got := "goodbye world", string(read); want != got { + t.Fatalf("Expected to read %v, got %v instead", want, got) + } + // END HACK +} + +// TestLifeOfAnApp installs an app, instantiates, runs, kills, and deletes +// several instances, and performs updates. +func TestLifeOfAnApp(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + // Get app publisher context (used later to publish apps) + var pubCtx *context.T + var err error + if pubCtx, err = setupPublishingCredentials(ctx); err != nil { + t.Fatal(err) + } + + sh, deferFn := servicetest.CreateShellAndMountTable(t, ctx) + defer deferFn() + + // Set up mock application and binary repositories. + envelope, cleanup := utiltest.StartMockRepos(t, ctx) + defer cleanup() + + root, cleanup := servicetest.SetupRootDir(t, "devicemanager") + defer cleanup() + if err := versioning.SaveCreatorInfo(ctx, root); err != nil { + t.Fatal(err) + } + + // Create a script wrapping the test target that implements suidhelper. + helperPath := utiltest.GenerateSuidHelperScript(t, root) + + // Set up the device manager. Since we won't do device manager updates, + // don't worry about its application envelope and current link. + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link") + dm.Start() + dm.S.Expect("READY") + utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken) + + // Create the local server that the app uses to let us know it's ready. + pingCh, cleanup := utiltest.SetupPingServer(t, ctx) + defer cleanup() + + utiltest.Resolve(t, ctx, "pingserver", 1, true) + + // Create an envelope for a first version of the app. + e, err := utiltest.SignedEnvelopeFromShell(pubCtx, sh, []string{utiltest.TestEnvVarName + "=env-val-envelope"}, []string{fmt.Sprintf("--%s=flag-val-envelope", utiltest.TestFlagName)}, utiltest.App, "google naps", 0, 0, "appV1") + if err != nil { + t.Fatalf("Unable to get signed envelope: %v", err) + } + *envelope = e + + // Install the app. The config-specified flag value for testFlagName + // should override the value specified in the envelope above, and the + // config-specified value for origin should override the value in the + // Install rpc argument. + mtName, ok := sh.Vars[ref.EnvNamespacePrefix] + if !ok { + t.Fatalf("failed to get namespace root var from shell") + } + // This rooted name should be equivalent to the relative name "ar", but + // we want to test that the config override for origin works. + rootedAppRepoName := naming.Join(mtName, "ar") + appID := utiltest.InstallApp(t, ctx, device.Config{utiltest.TestFlagName: "flag-val-install", mgmt.AppOriginConfigKey: rootedAppRepoName}) + v1 := utiltest.VerifyState(t, ctx, device.InstallationStateActive, appID) + installationDebug := utiltest.Debug(t, ctx, appID) + // We spot-check a couple pieces of information we expect in the debug + // output. + // TODO(caprita): Is there a way to verify more without adding brittle + // logic that assumes too much about the format? This may be one + // argument in favor of making the output of Debug a struct instead of + // free-form string. + if !strings.Contains(installationDebug, fmt.Sprintf("Origin: %v", rootedAppRepoName)) { + t.Fatalf("debug response doesn't contain expected info: %v", installationDebug) + } + if !strings.Contains(installationDebug, "Config: map[random_test_flag:flag-val-install]") { + t.Fatalf("debug response doesn't contain expected info: %v", installationDebug) + } + + // Start requires the caller to bless the app instance. + expectedErr := "bless failed" + if _, err := utiltest.LaunchAppImpl(t, ctx, appID, ""); err == nil || err.Error() != expectedErr { + t.Fatalf("Start(%v) expected to fail with %v, got %v instead", appID, expectedErr, err) + } + + // Start an instance of the app. + instance1ID := utiltest.LaunchApp(t, ctx, appID) + if v := utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance1ID); v != v1 { + t.Fatalf("Instance version expected to be %v, got %v instead", v1, v) + } + + instanceDebug := utiltest.Debug(t, ctx, appID, instance1ID) + + // Verify the app's default blessings. + if def, _ := v23.GetPrincipal(ctx).BlessingStore().Default(); !strings.Contains(instanceDebug, fmt.Sprintf("Default Blessings %s:forapp", def)) { + t.Fatalf("debug response doesn't contain expected info: %v", instanceDebug) + } + + // Verify the "..." blessing, which will include the publisher blessings + verifyAppPeerBlessings(t, ctx, pubCtx, instanceDebug, envelope) + + // Wait until the app pings us that it's ready. + pingResult := pingCh.VerifyPingArgs(t, utiltest.UserName(t), "flag-val-install", "env-val-envelope") + v1EP1 := utiltest.Resolve(t, ctx, "appV1", 1, true)[0] + + // Check that the instance name handed to the app looks plausible + nameRE := regexp.MustCompile(".*/apps/google naps/[^/]+/[^/]+$") + if nameRE.FindString(pingResult.InstanceName) == "" { + t.Fatalf("Unexpected instance name: %v", pingResult.InstanceName) + } + + // There should be at least one publisher blessing prefix, and all prefixes should + // end in ":mydevice" because they are just the device manager's blessings + prefixes := strings.Split(pingResult.PubBlessingPrefixes, ",") + if len(prefixes) == 0 { + t.Fatalf("No publisher blessing prefixes found: %v", pingResult) + } + for _, p := range prefixes { + if !strings.HasSuffix(p, ":mydevice") { + t.Fatalf("publisher Blessing prefixes don't look right: %v", pingResult.PubBlessingPrefixes) + } + } + + // We used a signed envelope, so there should have been some publisher blessings + if !hasPrefixMatches(pingResult.PubBlessingPrefixes, pingResult.DefaultPeerBlessings) { + t.Fatalf("Publisher Blessing Prefixes are not as expected: %v vs %v", pingResult.PubBlessingPrefixes, pingResult.DefaultPeerBlessings) + } + + // Stop the app instance. + utiltest.KillApp(t, ctx, appID, instance1ID) + utiltest.VerifyState(t, ctx, device.InstanceStateNotRunning, appID, instance1ID) + utiltest.ResolveExpectNotFound(t, ctx, "appV1", true) + + utiltest.RunApp(t, ctx, appID, instance1ID) + utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance1ID) + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready. + oldV1EP1 := v1EP1 + if v1EP1 = utiltest.Resolve(t, ctx, "appV1", 1, true)[0]; v1EP1 == oldV1EP1 { + t.Fatalf("Expected a new endpoint for the app after kill/run") + } + + // Start a second instance. + instance2ID := utiltest.LaunchApp(t, ctx, appID) + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready. + + // There should be two endpoints mounted as "appV1", one for each + // instance of the app. + endpoints := utiltest.Resolve(t, ctx, "appV1", 2, true) + v1EP2 := endpoints[0] + if endpoints[0] == v1EP1 { + v1EP2 = endpoints[1] + if v1EP2 == v1EP1 { + t.Fatalf("Both endpoints are the same") + } + } else if endpoints[1] != v1EP1 { + t.Fatalf("Second endpoint should have been v1EP1: %v, %v", endpoints, v1EP1) + } + + // TODO(caprita): verify various non-standard combinations (kill when + // canceled; run while still running). + + // Kill the first instance. + utiltest.KillApp(t, ctx, appID, instance1ID) + // Only the second instance should still be running and mounted. + // In this case, we don't want to retry since we shouldn't need to. + if want, got := v1EP2, utiltest.Resolve(t, ctx, "appV1", 1, false)[0]; want != got { + t.Fatalf("Resolve(%v): want: %v, got %v", "appV1", want, got) + } + + // Updating the installation to itself is a no-op. + utiltest.UpdateAppExpectError(t, ctx, appID, errors.ErrUpdateNoOp.ID) + + // Updating the installation should not work with a mismatched title. + *envelope = utiltest.EnvelopeFromShell(sh, nil, nil, utiltest.App, "bogus", 0, 0, "bogus") + + utiltest.UpdateAppExpectError(t, ctx, appID, errors.ErrAppTitleMismatch.ID) + + // Create a second version of the app and update the app to it. + *envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-val-envelope"}, nil, utiltest.App, "google naps", 0, 0, "appV2") + + utiltest.UpdateApp(t, ctx, appID) + + v2 := utiltest.VerifyState(t, ctx, device.InstallationStateActive, appID) + if v1 == v2 { + t.Fatalf("Version did not change for %v: %v", appID, v1) + } + + // Second instance should still be running, don't retry. + if want, got := v1EP2, utiltest.Resolve(t, ctx, "appV1", 1, false)[0]; want != got { + t.Fatalf("Resolve(%v): want: %v, got %v", "appV1", want, got) + } + if v := utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance2ID); v != v1 { + t.Fatalf("Instance version expected to be %v, got %v instead", v1, v) + } + + // Resume first instance. + utiltest.RunApp(t, ctx, appID, instance1ID) + if v := utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance1ID); v != v1 { + t.Fatalf("Instance version expected to be %v, got %v instead", v1, v) + } + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready. + // Both instances should still be running the first version of the app. + // Check that the mounttable contains two endpoints, one of which is + // v1EP2. + endpoints = utiltest.Resolve(t, ctx, "appV1", 2, true) + if endpoints[0] == v1EP2 { + if endpoints[1] == v1EP2 { + t.Fatalf("Both endpoints are the same") + } + } else if endpoints[1] != v1EP2 { + t.Fatalf("Second endpoint should have been v1EP2: %v, %v", endpoints, v1EP2) + } + + // Trying to update first instance while it's running should fail. + utiltest.UpdateInstanceExpectError(t, ctx, appID, instance1ID, errors.ErrInvalidOperation.ID) + // Stop first instance and try again. + utiltest.KillApp(t, ctx, appID, instance1ID) + // Only the second instance should still be running and mounted, don't retry. + if want, got := v1EP2, utiltest.Resolve(t, ctx, "appV1", 1, false)[0]; want != got { + t.Fatalf("Resolve(%v): want: %v, got %v", "appV1", want, got) + } + // Update succeeds now. + utiltest.UpdateInstance(t, ctx, appID, instance1ID) + if v := utiltest.VerifyState(t, ctx, device.InstanceStateNotRunning, appID, instance1ID); v != v2 { + t.Fatalf("Instance version expected to be %v, got %v instead", v2, v) + } + // Resume the first instance and verify it's running v2 now. + utiltest.RunApp(t, ctx, appID, instance1ID) + pingResult = pingCh.VerifyPingArgs(t, utiltest.UserName(t), "flag-val-install", "env-val-envelope") + utiltest.Resolve(t, ctx, "appV1", 1, false) + utiltest.Resolve(t, ctx, "appV2", 1, false) + + // Although v2 does not have a signed envelope, this was an update of v1, which did. + // This app's config still includes publisher blessing prefixes, and it should still + // have the publisher blessing it acquired earlier. + // + // TODO: This behavior is non-ideal. A reasonable requirement in future would be that + // the publisher blessing string remain unchanged on updates to an installation, just as the + // title is not allowed to change. + if !hasPrefixMatches(pingResult.PubBlessingPrefixes, pingResult.DefaultPeerBlessings) { + t.Fatalf("Publisher Blessing Prefixes are not as expected: %v vs %v", pingResult.PubBlessingPrefixes, pingResult.DefaultPeerBlessings) + } + + // Reverting first instance fails since it's still running. + utiltest.RevertAppExpectError(t, ctx, appID+"/"+instance1ID, errors.ErrInvalidOperation.ID) + // Stop first instance and try again. + utiltest.KillApp(t, ctx, appID, instance1ID) + verifyAppWorkspace(t, root, appID, instance1ID) + utiltest.ResolveExpectNotFound(t, ctx, "appV2", true) + utiltest.RevertApp(t, ctx, appID+"/"+instance1ID) + // Resume the first instance and verify it's running v1 now. + utiltest.RunApp(t, ctx, appID, instance1ID) + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "flag-val-install", "env-val-envelope") + utiltest.Resolve(t, ctx, "appV1", 2, false) + utiltest.TerminateApp(t, ctx, appID, instance1ID) + utiltest.Resolve(t, ctx, "appV1", 1, false) + + // Start a third instance. + instance3ID := utiltest.LaunchApp(t, ctx, appID) + if v := utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance3ID); v != v2 { + t.Fatalf("Instance version expected to be %v, got %v instead", v2, v) + } + // Wait until the app pings us that it's ready. + pingResult = pingCh.VerifyPingArgs(t, utiltest.UserName(t), "flag-val-install", "env-val-envelope") + // This app should not have publisher blessings. It was started from an installation + // that did not have a signed envelope. + if hasPrefixMatches(pingResult.PubBlessingPrefixes, pingResult.DefaultPeerBlessings) { + t.Fatalf("Publisher Blessing Prefixes are not as expected: %v vs %v", pingResult.PubBlessingPrefixes, pingResult.DefaultPeerBlessings) + } + + utiltest.Resolve(t, ctx, "appV2", 1, true) + + // Suspend second instance. + utiltest.KillApp(t, ctx, appID, instance2ID) + utiltest.ResolveExpectNotFound(t, ctx, "appV1", true) + + // Reverting second instance is a no-op since it's already running v1. + utiltest.RevertAppExpectError(t, ctx, appID+"/"+instance2ID, errors.ErrUpdateNoOp.ID) + + // Stop third instance. + utiltest.TerminateApp(t, ctx, appID, instance3ID) + utiltest.ResolveExpectNotFound(t, ctx, "appV2", true) + + // Revert the app. + utiltest.RevertApp(t, ctx, appID) + if v := utiltest.VerifyState(t, ctx, device.InstallationStateActive, appID); v != v1 { + t.Fatalf("Installation version expected to be %v, got %v instead", v1, v) + } + + // Start a fourth instance. It should be running from version 1. + instance4ID := utiltest.LaunchApp(t, ctx, appID) + if v := utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance4ID); v != v1 { + t.Fatalf("Instance version expected to be %v, got %v instead", v1, v) + } + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "flag-val-install", "env-val-envelope") // Wait until the app pings us that it's ready. + utiltest.Resolve(t, ctx, "appV1", 1, true) + utiltest.TerminateApp(t, ctx, appID, instance4ID) + utiltest.ResolveExpectNotFound(t, ctx, "appV1", true) + + // We are already on the first version, no further revert possible. + utiltest.RevertAppExpectError(t, ctx, appID, errors.ErrUpdateNoOp.ID) + + // Uninstall the app. + utiltest.UninstallApp(t, ctx, appID) + utiltest.VerifyState(t, ctx, device.InstallationStateUninstalled, appID) + + // Updating the installation should no longer be allowed. + utiltest.UpdateAppExpectError(t, ctx, appID, errors.ErrInvalidOperation.ID) + + // Reverting the installation should no longer be allowed. + utiltest.RevertAppExpectError(t, ctx, appID, errors.ErrInvalidOperation.ID) + + // Starting new instances should no longer be allowed. + utiltest.LaunchAppExpectError(t, ctx, appID, errors.ErrInvalidOperation.ID) + + // Make sure that Kill will actually kill an app that doesn't exit + // cleanly Do this by installing, instantiating, running, and killing + // hangingApp, which sleeps (rather than exits) after being asked to + // Stop() + *envelope = utiltest.EnvelopeFromShell(sh, nil, nil, utiltest.HangingApp, "hanging app", 0, 0, "hAppV1") + hAppID := utiltest.InstallApp(t, ctx) + hInstanceID := utiltest.LaunchApp(t, ctx, hAppID) + hangingPid := pingCh.WaitForPingArgs(t).Pid + if err := syscall.Kill(hangingPid, 0); err != nil && err != syscall.EPERM { + t.Fatalf("Pid of hanging app (%v) is not live", hangingPid) + } + utiltest.KillApp(t, ctx, hAppID, hInstanceID) + pidIsAlive := true + for i := 0; i < 10 && pidIsAlive; i++ { + if err := syscall.Kill(hangingPid, 0); err == nil || err == syscall.EPERM { + time.Sleep(time.Second) // pid is still alive + } else { + pidIsAlive = false + } + } + if pidIsAlive { + t.Fatalf("Pid of hanging app (%d) has not exited after Stop() call", hangingPid) + } + + // In the first pass, TidyNow (below), finds that everything should be too + // young to be tidied becasue TidyNow's first call to MockableNow() + // provides the current time. + shouldKeepInstances := keepAll(t, root, filepath.Join(root, "app*", "installation*", "instances", "instance*")) + shouldKeepInstallations := keepAll(t, root, filepath.Join(root, "app*", "installation*")) + shouldKeepLogFiles := keepAll(t, root, filepath.Join(root, "app*", "installation*", "instances", "instance*", "logs", "*")) + + if err := utiltest.DeviceStub("dm").TidyNow(ctx); err != nil { + t.Fatalf("TidyNow failed: %v", err) + } + + verifyTidying(t, root, filepath.Join(root, "app*", "installation*", "instances", "instance*"), shouldKeepInstances) + verifyTidying(t, root, filepath.Join(root, "app*", "installation*"), shouldKeepInstallations) + verifyTidying(t, root, filepath.Join(root, "app*", "installation*", "instances", "instance*", "logs", "*"), shouldKeepLogFiles) + + // In the second pass, TidyNow() (below) calls MockableNow() again + // which has advanced to tomorrow so it should find that all items have + // become old enough to tidy. + shouldKeepInstances = determineShouldKeep(t, root, filepath.Join(root, "app*", "installation*", "instances", "instance*"), "Deleted") + shouldKeepInstallations = addBackLinks(t, root, determineShouldKeep(t, root, filepath.Join(root, "app*", "installation*"), "Uninstalled")) + shouldKeepLogFiles = determineLogFilesToKeep(t, shouldKeepInstances) + + if err := utiltest.DeviceStub("dm").TidyNow(ctx); err != nil { + t.Fatalf("TidyNow failed: %v", err) + } + + verifyTidying(t, root, filepath.Join(root, "app*", "installation*", "instances", "instance*"), shouldKeepInstances) + verifyTidying(t, root, filepath.Join(root, "app*", "installation*"), shouldKeepInstallations) + verifyTidying(t, root, filepath.Join(root, "app*", "installation*", "instances", "instance*", "logs", "*"), shouldKeepLogFiles) + + // Cleanly shut down the device manager. + dm.Terminate(os.Interrupt) + dm.S.Expect("dm terminated") + utiltest.VerifyNoRunningProcesses(t) +} + +func keepAll(t *testing.T, root, globpath string) map[string]bool { + paths, err := filepath.Glob(globpath) + if err != nil { + t.Errorf("keepAll %v", err) + } + shouldKeep := make(map[string]bool) + for _, idir := range paths { + shouldKeep[idir] = true + } + return shouldKeep +} + +func determineShouldKeep(t *testing.T, root, globpath, state string) map[string]bool { + paths, err := filepath.Glob(globpath) + if err != nil { + t.Errorf("determineShouldKeep %v", err) + } + + shouldKeep := make(map[string]bool) + for _, idir := range paths { + p := filepath.Join(idir, state) + _, err := os.Stat(p) + if os.IsNotExist(err) { + shouldKeep[idir] = true + } else if err == nil { + shouldKeep[idir] = false + } else { + t.Errorf("determineShouldKeep Stat(%s) failed: %v", p, err) + } + } + return shouldKeep + +} + +func addBackLinks(t *testing.T, root string, installationShouldKeep map[string]bool) map[string]bool { + paths, err := filepath.Glob(filepath.Join(root, "app*", "installation*", "instances", "instance*", "installation")) + if err != nil { + t.Errorf("addBackLinks %v", err) + } + + for _, idir := range paths { + pth, err := os.Readlink(idir) + if err != nil { + t.Errorf("addBackLinks %v", err) + continue + } + if _, ok := installationShouldKeep[pth]; ok { + // An instance symlinks to this pth so must be kept. + installationShouldKeep[pth] = true + } + } + return installationShouldKeep +} + +// determineLogFilesToKeep produces a map of the log files that +// should remain after tidying. It returns a map to be compatible +// with the verifyTidying. +func determineLogFilesToKeep(t *testing.T, instances map[string]bool) map[string]bool { + shouldKeep := make(map[string]bool) + for idir, keep := range instances { + if !keep { + continue + } + + paths, err := filepath.Glob(filepath.Join(idir, "logs", "*")) + if err != nil { + t.Errorf("determineLogFilesToKeep filepath.Glob(%s) failed: %v", idir, err) + return shouldKeep + } + + for _, p := range paths { + fi, err := os.Stat(p) + if err != nil { + t.Errorf("determineLogFilesToKeep os.Stat(%s): %v", p, err) + return shouldKeep + } + + if fi.Mode()&os.ModeSymlink == 0 { + continue + } + + shouldKeep[p] = true + target, err := os.Readlink(p) + if err != nil { + t.Errorf("determineLogFilesToKeep os.Readlink(%s): %v", p, err) + return shouldKeep + } + shouldKeep[target] = true + } + } + return shouldKeep +} + +func verifyTidying(t *testing.T, root, globpath string, shouldKeep map[string]bool) { + paths, err := filepath.Glob(globpath) + if err != nil { + t.Errorf("verifyTidying %v", err) + } + + // TidyUp adds nothing: pth should be a subset of shouldKeep. + for _, pth := range paths { + if !shouldKeep[pth] { + t.Errorf("TidyUp (%s) wrongly added path: %s", globpath, pth) + return + } + } + + // Tidy should not leave unkept instances: shouldKeep ^ pth should be entirely true. + for _, pth := range paths { + if !shouldKeep[pth] { + t.Errorf("TidyUp (%s) failed to delete: %s", globpath, pth) + return + } + } + + // Tidy must not delete any kept instances. + for k, v := range shouldKeep { + if v { + if _, err := os.Stat(k); os.IsNotExist(err) { + t.Errorf("TidyUp (%s) deleted an instance it shouldn't have: %s", globpath, k) + } + } + } +} + +// setupPublishingCredentials creates two principals, which, in addition to the one passed in +// (which is "the user") allow us to have an "identity provider", and a "publisher". The +// user and the publisher are both blessed by the identity provider. The return value is +// a context that can be used to publish an envelope with a signed binary. +func setupPublishingCredentials(ctx *context.T) (*context.T, error) { + IDPPrincipal := testutil.NewPrincipal("identitypro") + IDPBlessing, _ := IDPPrincipal.BlessingStore().Default() + + PubPrincipal := testutil.NewPrincipal() + UserPrincipal := v23.GetPrincipal(ctx) + + var b security.Blessings + var c security.Caveat + var err error + if c, err = security.NewExpiryCaveat(time.Now().Add(time.Hour * 24 * 30)); err != nil { + return nil, err + } + if b, err = IDPPrincipal.Bless(UserPrincipal.PublicKey(), IDPBlessing, "u:alice", c); err != nil { + return nil, err + } + if err := vsecurity.SetDefaultBlessings(UserPrincipal, b); err != nil { + return nil, err + } + + if b, err = IDPPrincipal.Bless(PubPrincipal.PublicKey(), IDPBlessing, "m:publisher", security.UnconstrainedUse()); err != nil { + return nil, err + } + if err := vsecurity.SetDefaultBlessings(PubPrincipal, b); err != nil { + return nil, err + } + + var pubCtx *context.T + if pubCtx, err = v23.WithPrincipal(ctx, PubPrincipal); err != nil { + return nil, err + } + + return pubCtx, nil +} + +// findPrefixMatches takes a set of comma-separated prefixes, and a set of comma-separated +// strings, and checks if any of the strings match any of the prefixes +func hasPrefixMatches(prefixList, stringList string) bool { + prefixes := strings.Split(prefixList, ",") + inStrings := strings.Split(stringList, ",") + + for _, s := range inStrings { + for _, p := range prefixes { + if strings.HasPrefix(s, p) { + return true + } + } + } + return false +} + +// verifyAppPeerBlessings checks the instanceDebug string to ensure that the app is running with +// the expected blessings for peer "..." (i.e. security.AllPrincipals) . +// +// The app should have one blessing that came from the user, of the form +// <base_blessing>:forapp. It should also have one or more publisher blessings, that are the +// cross product of the device manager blessings and the publisher blessings in the app +// envelope. +func verifyAppPeerBlessings(t *testing.T, ctx, pubCtx *context.T, instanceDebug string, e *application.Envelope) { + // Extract the blessings from the debug output + // + // TODO(caprita): This is flaky, since the '...' peer pattern may not be + // the first one in the sorted pattern list. See v.io/i/680 + blessingRE := regexp.MustCompile(`Blessings\s?\n\s?\.\.\.\s*([^\n]+)`) + blessingMatches := blessingRE.FindStringSubmatch(instanceDebug) + if len(blessingMatches) < 2 { + t.Fatalf("Failed to match blessing regex: [%v] [%v]", blessingMatches, instanceDebug) + } + blessingList := strings.Split(blessingMatches[1], ",") + + // Compute a map of the blessings we expect to find + expBlessings := make(map[string]bool) + baseBlessing, _ := v23.GetPrincipal(ctx).BlessingStore().Default() + expBlessings[baseBlessing.String()+":forapp"] = false + + // App blessings should be the cross product of device manager and publisher blessings + + // dmBlessings below is a slice even though we have just one entry because we'll likely + // want it to have more than one in future. (Today, a device manager typically has a + // blessing from its claimer, but in many cases there might be other blessings too, such + // as one from the manufacturer, or one from the organization that owns the device.) + dmBlessings := []string{baseBlessing.String() + ":mydevice"} + pubBlessings := strings.Split(e.Publisher.String(), ",") + for _, dmb := range dmBlessings { + for _, pb := range pubBlessings { + expBlessings[dmb+":a:"+pb] = false + } + } + + // Check the list of blessings against the map of expected blessings + matched := 0 + for _, b := range blessingList { + if seen, ok := expBlessings[b]; ok && !seen { + expBlessings[b] = true + matched++ + } + } + if matched != len(expBlessings) { + t.Fatalf("Missing some blessings in set %v. App blessings were: %v", expBlessings, blessingList) + } +} diff --git a/x/ref/services/device/deviced/internal/impl/applife/impl_test.go b/x/ref/services/device/deviced/internal/impl/applife/impl_test.go new file mode 100644 index 000000000..0808c657e --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/applife/impl_test.go @@ -0,0 +1,19 @@ +// Copyright 2015 The Vanadium 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 applife_test + +import ( + "testing" + + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" +) + +func TestMain(m *testing.M) { + utiltest.TestMainImpl(m) +} + +func TestSuidHelper(t *testing.T) { + utiltest.TestSuidHelperImpl(t) +} diff --git a/x/ref/services/device/deviced/internal/impl/applife/instance_reaping_test.go b/x/ref/services/device/deviced/internal/impl/applife/instance_reaping_test.go new file mode 100644 index 000000000..9a26445db --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/applife/instance_reaping_test.go @@ -0,0 +1,80 @@ +// Copyright 2015 The Vanadium 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 applife_test + +import ( + "os" + "syscall" + "testing" + + "v.io/v23/naming" + "v.io/v23/services/device" + "v.io/v23/services/stats" + "v.io/v23/vdl" + + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" +) + +func TestReaperNoticesAppDeath(t *testing.T) { + cleanup, ctx, sh, envelope, root, helperPath, _ := utiltest.StartupHelper(t) + defer cleanup() + + // Set up the device manager. Since we won't do device manager updates, + // don't worry about its application envelope and current link. + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link") + dm.Start() + dm.S.Expect("READY") + utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken) + + // Create the local server that the app uses to let us know it's ready. + pingCh, cleanup := utiltest.SetupPingServer(t, ctx) + defer cleanup() + + utiltest.Resolve(t, ctx, "pingserver", 1, true) + + // Create an envelope for a first version of the app. + *envelope = utiltest.EnvelopeFromShell(sh, nil, nil, utiltest.App, "google naps", 0, 0, "appV1") + + // Install the app. The config-specified flag value for testFlagName + // should override the value specified in the envelope above. + appID := utiltest.InstallApp(t, ctx) + + // Start an instance of the app. + instance1ID := utiltest.LaunchApp(t, ctx, appID) + + // Wait until the app pings us that it's ready. + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "default", "") + + // Get application pid. + name := naming.Join("dm", "apps/"+appID+"/"+instance1ID+"/stats/system/pid") + c := stats.StatsClient(name) + v, err := c.Value(ctx) + if err != nil { + t.Fatalf("Value() failed: %v\n", err) + } + var pid int + if err := vdl.Convert(&pid, v); err != nil { + t.Fatalf("pid returned from stats interface is not an int: %v", err) + } + + utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance1ID) + syscall.Kill(int(pid), 9) + + // Start a second instance of the app which will force polling to happen. + instance2ID := utiltest.LaunchApp(t, ctx, appID) + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "default", "") + + utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instance2ID) + + utiltest.TerminateApp(t, ctx, appID, instance2ID) + utiltest.VerifyState(t, ctx, device.InstanceStateNotRunning, appID, instance1ID) + + // TODO(rjkroege): Exercise the polling loop code. + + // Cleanly shut down the device manager. + dm.Terminate(os.Interrupt) + dm.S.Expect("dm terminated") + utiltest.VerifyNoRunningProcesses(t) +} diff --git a/x/ref/services/device/deviced/internal/impl/associate_instance_test.go b/x/ref/services/device/deviced/internal/impl/associate_instance_test.go new file mode 100644 index 000000000..3078cb592 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/associate_instance_test.go @@ -0,0 +1,32 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "io/ioutil" + "os" + "testing" +) + +func TestSystemNameState(t *testing.T) { + dir, err := ioutil.TempDir("", "instance") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(dir) + + expected := "vanadium-user" + if err := saveSystemNameForInstance(dir, expected); err != nil { + t.Fatalf("saveSystemNameForInstance(%v, %v) failed: %v", dir, expected, err) + } + + got, err := readSystemNameForInstance(dir) + if err != nil { + t.Fatalf("readSystemNameForInstance(%v) failed: ", err) + } + if got != expected { + t.Fatalf("got %v, expected %v", got, expected) + } +} diff --git a/x/ref/services/device/deviced/internal/impl/association_instance.go b/x/ref/services/device/deviced/internal/impl/association_instance.go new file mode 100644 index 000000000..0f15505a0 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/association_instance.go @@ -0,0 +1,34 @@ +// Copyright 2015 The Vanadium 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 impl + +// Code to manage the persistence of which systemName is associated with +// a given application instance. + +import ( + "fmt" + "io/ioutil" + "path/filepath" + + "v.io/v23/verror" + "v.io/x/ref/services/device/internal/errors" +) + +func saveSystemNameForInstance(dir, systemName string) error { + snp := filepath.Join(dir, "systemname") + if err := ioutil.WriteFile(snp, []byte(systemName), 0600); err != nil { + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("WriteFile(%v, %v) failed: %v", snp, systemName, err)) + } + return nil +} + +func readSystemNameForInstance(dir string) (string, error) { + snp := filepath.Join(dir, "systemname") + name, err := ioutil.ReadFile(snp) + if err != nil { + return "", verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("ReadFile(%v) failed: %v", snp, err)) + } + return string(name), nil +} diff --git a/x/ref/services/device/deviced/internal/impl/association_state.go b/x/ref/services/device/deviced/internal/impl/association_state.go new file mode 100644 index 000000000..2f25912db --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/association_state.go @@ -0,0 +1,128 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "encoding/json" + "os" + "path/filepath" + "sync" + + "v.io/v23/services/device" + "v.io/v23/verror" +) + +// BlessingSystemAssociationStore manages a persisted association between +// Vanadium blessings and system account names. +type BlessingSystemAssociationStore interface { + // SystemAccountForBlessings returns a system name from the blessing to + // system name association store if one exists for any of the listed + // blessings. + SystemAccountForBlessings(blessings []string) (string, bool) + + // AllBlessingSystemAssociations returns all of the current Blessing to system + // account associations. + AllBlessingSystemAssociations() ([]device.Association, error) + + // AssociateSystemAccountForBlessings associates the provided systenName with each + // provided blessing. + AssociateSystemAccountForBlessings(blessings []string, systemName string) error + + // DisassociateSystemAccountForBlessings removes associations for the provided blessings. + DisassociateSystemAccountForBlessings(blessings []string) error +} + +type association struct { + data map[string]string + filename string + sync.Mutex +} + +func (u *association) SystemAccountForBlessings(blessings []string) (string, bool) { + u.Lock() + defer u.Unlock() + + systemName := "" + present := false + + for _, n := range blessings { + if systemName, present = u.data[n]; present { + break + } + } + return systemName, present +} + +func (u *association) AllBlessingSystemAssociations() ([]device.Association, error) { + u.Lock() + defer u.Unlock() + assocs := make([]device.Association, 0) + + for k, v := range u.data { + assocs = append(assocs, device.Association{k, v}) + } + return assocs, nil +} + +func (u *association) serialize() (err error) { + f, err := os.OpenFile(u.filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return verror.New(verror.ErrNoExist, nil, "Could not open association file for writing", u.filename, err) + } + defer func() { + if closerr := f.Close(); closerr != nil { + err = closerr + } + }() + + enc := json.NewEncoder(f) + return enc.Encode(u.data) +} + +func (u *association) AssociateSystemAccountForBlessings(blessings []string, systemName string) error { + u.Lock() + defer u.Unlock() + + for _, n := range blessings { + u.data[n] = systemName + } + return u.serialize() +} + +func (u *association) DisassociateSystemAccountForBlessings(blessings []string) error { + u.Lock() + defer u.Unlock() + + for _, n := range blessings { + delete(u.data, n) + } + return u.serialize() +} + +func NewBlessingSystemAssociationStore(root string) (BlessingSystemAssociationStore, error) { + nddir := filepath.Join(root, "device-manager", "device-data") + if err := os.MkdirAll(nddir, os.FileMode(0700)); err != nil { + return nil, verror.New(verror.ErrNoExist, nil, "Could not create device-data directory", nddir, err) + } + msf := filepath.Join(nddir, "associated.accounts") + + f, err := os.Open(msf) + if err != nil && os.IsExist(err) { + return nil, verror.New(verror.ErrNoExist, nil, "Could not open association file", msf, err) + + } + defer f.Close() + + a := &association{filename: msf, data: make(map[string]string)} + + if err == nil { + dec := json.NewDecoder(f) + err := dec.Decode(&a.data) + if err != nil { + return nil, verror.New(verror.ErrNoExist, nil, "Could not read association file", msf, err) + } + } + return BlessingSystemAssociationStore(a), nil +} diff --git a/x/ref/services/device/deviced/internal/impl/association_state_test.go b/x/ref/services/device/deviced/internal/impl/association_state_test.go new file mode 100644 index 000000000..055e53263 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/association_state_test.go @@ -0,0 +1,170 @@ +// Copyright 2015 The Vanadium 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 impl_test + +import ( + "io" + "io/ioutil" + "os" + "path" + "testing" + + "v.io/v23/services/device" + + "v.io/x/ref/services/device/deviced/internal/impl" + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" +) + +// TestAssociationPersistence verifies correct operation of association +// persistence code. +func TestAssociationPersistence(t *testing.T) { + td, err := ioutil.TempDir("", "device_test") + if err != nil { + t.Fatalf("TempDir failed: %v", err) + } + defer os.RemoveAll(td) + nbsa1, err := impl.NewBlessingSystemAssociationStore(td) + if err != nil { + t.Fatalf("NewBlessingSystemAssociationStore failed: %v", err) + } + + // Insert an association. + err = nbsa1.AssociateSystemAccountForBlessings([]string{"alice", "bob"}, "alice_account") + if err != nil { + t.Fatalf("AssociateSystemAccount failed: %v", err) + } + + got1, err := nbsa1.AllBlessingSystemAssociations() + if err != nil { + t.Fatalf("AllBlessingSystemAssociations failed: %v", err) + } + + utiltest.CompareAssociations(t, got1, []device.Association{ + { + "alice", + "alice_account", + }, + { + "bob", + "alice_account", + }, + }) + + nbsa2, err := impl.NewBlessingSystemAssociationStore(td) + if err != nil { + t.Fatalf("NewBlessingSystemAssociationStore failed: %v", err) + } + + got2, err := nbsa2.AllBlessingSystemAssociations() + if err != nil { + t.Fatalf("AllBlessingSystemAssociations failed: %v", err) + } + utiltest.CompareAssociations(t, got1, got2) + + sysacc, have := nbsa2.SystemAccountForBlessings([]string{"bob"}) + if expected := true; have != expected { + t.Fatalf("SystemAccountForBlessings failed. got %v, expected %v", have, expected) + } + if expected := "alice_account"; sysacc != expected { + t.Fatalf("SystemAccountForBlessings failed. got %v, expected %v", sysacc, expected) + } + + sysacc, have = nbsa2.SystemAccountForBlessings([]string{"doug"}) + if expected := false; have != expected { + t.Fatalf("SystemAccountForBlessings failed. got %v, expected %v", have, expected) + } + if expected := ""; sysacc != expected { + t.Fatalf("SystemAccountForBlessings failed. got %v, expected %v", sysacc, expected) + } + + // Remove "bob". + err = nbsa1.DisassociateSystemAccountForBlessings([]string{"bob"}) + if err != nil { + t.Fatalf("DisassociateSystemAccountForBlessings failed: %v", err) + } + + // Verify that "bob" has been removed. + got1, err = nbsa1.AllBlessingSystemAssociations() + if err != nil { + t.Fatalf("AllBlessingSystemAssociations failed: %v", err) + } + utiltest.CompareAssociations(t, got1, []device.Association{ + { + "alice", + "alice_account", + }, + }) + + err = nbsa1.AssociateSystemAccountForBlessings([]string{"alice", "bob"}, "alice_other_account") + if err != nil { + t.Fatalf("AssociateSystemAccount failed: %v", err) + } + // Verify that "bob" and "alice" have new values. + got1, err = nbsa1.AllBlessingSystemAssociations() + if err != nil { + t.Fatalf("AllBlessingSystemAssociations failed: %v", err) + } + utiltest.CompareAssociations(t, got1, []device.Association{ + { + "alice", + "alice_other_account", + }, + { + "bob", + "alice_other_account", + }, + }) + + // Make future serialization attempts fail. + if err := os.RemoveAll(td); err != nil { + t.Fatalf("os.RemoveAll: couldn't delete %s: %v", td, err) + } + err = nbsa1.AssociateSystemAccountForBlessings([]string{"doug"}, "alice_account") + if err == nil { + t.Fatalf("AssociateSystemAccount should have failed but didn't") + } +} + +func TestAssociationPersistenceDetectsBadStartingConditions(t *testing.T) { + dir := "/i-am-hoping-that-there-is-no-such-directory" + nbsa1, err := impl.NewBlessingSystemAssociationStore(dir) + if nbsa1 != nil || err == nil { + t.Fatalf("bad root directory %s ought to have caused an error", dir) + } + + // Create a NewBlessingSystemAssociationStore directory as a side-effect. + dir, err = ioutil.TempDir("", "bad-starting-conditions") + if err != nil { + t.Fatalf("TempDir failed: %v", err) + } + nbsa1, err = impl.NewBlessingSystemAssociationStore(dir) + defer os.RemoveAll(dir) + if err != nil { + t.Fatalf("NewBlessingSystemAssociationStore failed: %v", err) + } + + tpath := path.Join(dir, "device-manager", "device-data", "associated.accounts") + f, err := os.Create(tpath) + if err != nil { + t.Fatalf("could not open backing file for setup: %v", err) + } + + if _, err := io.WriteString(f, "bad-json\""); err != nil { + t.Fatalf("could not write to test file %s: %v", tpath, err) + } + f.Close() + + nbsa1, err = impl.NewBlessingSystemAssociationStore(dir) + if nbsa1 != nil || err == nil { + t.Fatalf("invalid JSON ought to have caused an error") + } + + // This test will fail if executed as root or if your system is configured oddly. + unreadableFile := "/dev/autofs" + nbsa1, err = impl.NewBlessingSystemAssociationStore(unreadableFile) + if nbsa1 != nil || err == nil { + t.Fatalf("unreadable file %s ought to have caused an error", unreadableFile) + } +} diff --git a/x/ref/services/device/deviced/internal/impl/callback.go b/x/ref/services/device/deviced/internal/impl/callback.go new file mode 100644 index 000000000..29fecba6c --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/callback.go @@ -0,0 +1,33 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "v.io/v23/context" + "v.io/x/ref/lib/exec" + "v.io/x/ref/lib/mgmt" + "v.io/x/ref/services/device" +) + +// InvokeCallback provides the parent device manager with the given name (which +// is expected to be this device manager's object name). +func InvokeCallback(ctx *context.T, name string) { + config, err := exec.ReadConfigFromOSEnv() + if err != nil || config == nil { + return + } + // Device manager was started by self-update, notify the parent. + callbackName, err := config.Get(mgmt.ParentNameConfigKey) + if err != nil { + // Device manager was not started by self-update, return silently. + return + } + client := device.ConfigClient(callbackName) + ctx, cancel := context.WithTimeout(ctx, rpcContextTimeout) + defer cancel() + if err := client.Set(ctx, mgmt.ChildNameConfigKey, name); err != nil { + ctx.Fatalf("Set(%v, %v) failed: %v", mgmt.ChildNameConfigKey, name, err) + } +} diff --git a/x/ref/services/device/deviced/internal/impl/config_service.go b/x/ref/services/device/deviced/internal/impl/config_service.go new file mode 100644 index 000000000..6a83e987c --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/config_service.go @@ -0,0 +1,159 @@ +// Copyright 2015 The Vanadium 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 impl + +// The config invoker is responsible for answering calls to the config service +// run as part of the device manager. The config invoker converts RPCs to +// messages on channels that are used to listen on callbacks coming from child +// application instances. + +import ( + "fmt" + "strconv" + "sync" + "time" + + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/verror" + "v.io/x/ref/services/device/internal/errors" +) + +type callbackState struct { + sync.Mutex + // channels maps callback identifiers and config keys to channels that + // are used to communicate corresponding config values from child + // processes. + channels map[string]map[string]chan<- string + // nextCallbackID provides the next callback identifier to use as a key + // for the channels map. + nextCallbackID int64 + // name is the object name for making calls against the device manager's + // config service. + name string +} + +func newCallbackState(name string) *callbackState { + return &callbackState{ + channels: make(map[string]map[string]chan<- string), + name: name, + } +} + +// callbackListener abstracts out listening for values provided via the +// callback mechanism for a given key. +type callbackListener interface { + // waitForValue blocks until the value that this listener is expecting + // arrives, until the timeout expires, or until stop() is called + waitForValue(timeout time.Duration) (string, error) + // stop makes waitForValue return early + stop() + // cleanup cleans up any state used by the listener. Should be called + // when the listener is no longer needed. + cleanup() + // name returns the object name for the config service object that + // handles the key that the listener is listening for. + name() string +} + +// listener implements callbackListener +type listener struct { + ctx *context.T + id string + cs *callbackState + ch <-chan string + n string + stopper chan struct{} +} + +func (l *listener) waitForValue(timeout time.Duration) (string, error) { + select { + case value := <-l.ch: + return value, nil + case <-time.After(timeout): + return "", verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("Waiting for callback timed out after %v", timeout)) + case <-l.stopper: + return "", verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("Stopped while waiting for callback")) + } +} + +func (l *listener) stop() { + l.ctx.FlushLog() + close(l.stopper) +} + +func (l *listener) cleanup() { + l.cs.unregister(l.id) +} + +func (l *listener) name() string { + return l.n +} + +func (c *callbackState) listenFor(ctx *context.T, key string) callbackListener { + id := c.generateID() + callbackName := naming.Join(c.name, configSuffix, id) + // Make the channel buffered to avoid blocking the Set method when + // nothing is receiving on the channel. This happens e.g. when + // unregisterCallbacks executes before Set is called. + callbackChan := make(chan string, 1) + c.register(id, key, callbackChan) + stopchan := make(chan struct{}, 1) + return &listener{ + ctx: ctx, + id: id, + cs: c, + ch: callbackChan, + n: callbackName, + stopper: stopchan, + } +} + +func (c *callbackState) generateID() string { + c.Lock() + defer c.Unlock() + c.nextCallbackID++ + return strconv.FormatInt(c.nextCallbackID-1, 10) +} + +func (c *callbackState) register(id, key string, channel chan<- string) { + c.Lock() + defer c.Unlock() + if _, ok := c.channels[id]; !ok { + c.channels[id] = make(map[string]chan<- string) + } + c.channels[id][key] = channel +} + +func (c *callbackState) unregister(id string) { + c.Lock() + defer c.Unlock() + delete(c.channels, id) +} + +// configService implements the Device manager's Config interface. +type configService struct { + callback *callbackState + // Suffix contains an identifier for the channel corresponding to the + // request. + suffix string +} + +func (i *configService) Set(_ *context.T, _ rpc.ServerCall, key, value string) error { + id := i.suffix + i.callback.Lock() + if _, ok := i.callback.channels[id]; !ok { + i.callback.Unlock() + return verror.New(errors.ErrInvalidSuffix, nil) + } + channel, ok := i.callback.channels[id][key] + i.callback.Unlock() + if !ok { + return nil + } + channel <- value + return nil +} diff --git a/x/ref/services/device/deviced/internal/impl/daemonreap/daemon_reaping_test.go b/x/ref/services/device/deviced/internal/impl/daemonreap/daemon_reaping_test.go new file mode 100644 index 000000000..e176c9234 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/daemonreap/daemon_reaping_test.go @@ -0,0 +1,86 @@ +// Copyright 2015 The Vanadium 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 daemonreap_test + +import ( + "os" + "syscall" + "testing" + "time" + + "v.io/v23/services/device" + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" +) + +func TestDaemonRestart(t *testing.T) { + cleanup, ctx, sh, envelope, root, helperPath, _ := utiltest.StartupHelper(t) + defer cleanup() + + // Set up the device manager. Since we won't do device manager updates, + // don't worry about its application envelope and current link. + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link") + dm.Start() + dm.S.Expect("READY") + utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken) + + // Create the local server that the app uses to let us know it's ready. + pingCh, cleanup := utiltest.SetupPingServer(t, ctx) + defer cleanup() + + utiltest.Resolve(t, ctx, "pingserver", 1, true) + + const nRestarts = 5 + // Create an envelope for a first version of the app that will be restarted nRestarts times. + *envelope = utiltest.EnvelopeFromShell(sh, nil, nil, utiltest.App, "google naps", nRestarts, 10*time.Minute, "appV1") + appID := utiltest.InstallApp(t, ctx) + + // Start an instance of the app. + instanceID := utiltest.LaunchApp(t, ctx, appID) + + // Wait until the app pings us that it's ready. + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "default", "") + + // Get application pid. + pid := utiltest.GetPid(t, ctx, appID, instanceID) + + utiltest.VerifyState(t, ctx, device.InstanceStateRunning, appID, instanceID) + + for i := 0; i < nRestarts; i++ { + syscall.Kill(int(pid), 9) + utiltest.PollingWait(t, int(pid)) + + // instanceID should be restarted automatically. + + // Be sure to get the ping from the restarted application so + // that the app is running again before we ask for its status. + pingCh.WaitForPingArgs(t) + + // WaitForState must be done after WaitForPingArgs for the + // following reason: we need to make sure the app went through + // the restart already, otherwise, it might be still in state + // "running" since the reaper hasn't yet noticed it died. + utiltest.WaitForState(t, ctx, device.InstanceStateRunning, appID, instanceID) + // Get application pid. + pid = utiltest.GetPid(t, ctx, appID, instanceID) + } + + // Kill the application again. + syscall.Kill(int(pid), 9) + utiltest.PollingWait(t, int(pid)) + + // The reaper should no longer restart the application: + // instanceID is not running because it exceeded its restart limit. + utiltest.WaitForState(t, ctx, device.InstanceStateNotRunning, appID, instanceID) + // This clunky sleep helps ensure that the app stays dead (it briefly + // transitioned through state 'not running' as part of a restart, so we + // wait a bit to see if it stays dead). + time.Sleep(time.Second) + utiltest.VerifyState(t, ctx, device.InstanceStateNotRunning, appID, instanceID) + + // Cleanly shut down the device manager. + dm.Terminate(os.Interrupt) + dm.S.Expect("dm terminated") + utiltest.VerifyNoRunningProcesses(t) +} diff --git a/x/ref/services/device/deviced/internal/impl/daemonreap/impl_test.go b/x/ref/services/device/deviced/internal/impl/daemonreap/impl_test.go new file mode 100644 index 000000000..5f0ed8f56 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/daemonreap/impl_test.go @@ -0,0 +1,19 @@ +// Copyright 2015 The Vanadium 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 daemonreap_test + +import ( + "testing" + + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" +) + +func TestMain(m *testing.M) { + utiltest.TestMainImpl(m) +} + +func TestSuidHelper(t *testing.T) { + utiltest.TestSuidHelperImpl(t) +} diff --git a/x/ref/services/device/deviced/internal/impl/daemonreap/instance_reaping_kill_test.go b/x/ref/services/device/deviced/internal/impl/daemonreap/instance_reaping_kill_test.go new file mode 100644 index 000000000..e727e51ab --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/daemonreap/instance_reaping_kill_test.go @@ -0,0 +1,111 @@ +// Copyright 2015 The Vanadium 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 daemonreap_test + +import ( + "os" + "syscall" + "testing" + + "v.io/v23/services/device" + "v.io/x/ref" + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" +) + +func TestReapReconciliationViaKill(t *testing.T) { + cleanup, ctx, sh, envelope, root, helperPath, _ := utiltest.StartupHelper(t) + defer cleanup() + + // Start a device manager. + // (Since it will be restarted, use the VeyronCredentials environment + // to maintain the same set of credentials across runs) + dmCreds := utiltest.CreatePrincipal(t, sh) + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link") + dm.Vars[ref.EnvCredentials] = dmCreds + dm.Start() + dm.S.Expect("READY") + utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken) + + // Create the local server that the app uses to let us know it's ready. + pingCh, cleanup := utiltest.SetupPingServer(t, ctx) + defer cleanup() + utiltest.Resolve(t, ctx, "pingserver", 1, true) + + // Create an envelope for the app. + *envelope = utiltest.EnvelopeFromShell(sh, nil, nil, utiltest.App, "google naps", 0, 0, "appV1") + + // Install the app. + appID := utiltest.InstallApp(t, ctx) + + // Start three app instances. + instances := make([]string, 3) + for i, _ := range instances { + instances[i] = utiltest.LaunchApp(t, ctx, appID) + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "default", "") + } + + // Get pid of instance[0] + pid := utiltest.GetPid(t, ctx, appID, instances[0]) + + // Shutdown the first device manager. + dm.Terminate(os.Interrupt) + dm.S.Expect("dm terminated") + utiltest.ResolveExpectNotFound(t, ctx, "dm", false) // Ensure a clean slate. + + // Kill instance[0] and wait until it exits before proceeding. + syscall.Kill(pid, 9) + utiltest.PollingWait(t, pid) + + // Run another device manager to replace the dead one. + dm = utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link") + dm.Vars[ref.EnvCredentials] = dmCreds + dm.Start() + dm.S.Expect("READY") + utiltest.Resolve(t, ctx, "dm", 1, true) // Verify the device manager has published itself. + + // By now, we've reconciled the state of the tree with which processes + // are actually alive. instance-0 is not alive. + expected := []device.InstanceState{device.InstanceStateNotRunning, device.InstanceStateRunning, device.InstanceStateRunning} + for i, _ := range instances { + utiltest.VerifyState(t, ctx, expected[i], appID, instances[i]) + } + + // Start instance[0] over-again to show that an app marked not running + // by reconciliation can be restarted. + utiltest.RunApp(t, ctx, appID, instances[0]) + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "default", "") + + // Kill instance[1] + pid = utiltest.GetPid(t, ctx, appID, instances[1]) + syscall.Kill(pid, 9) + + // Make a fourth instance. This forces a polling of processes so that + // the state is updated. + instances = append(instances, utiltest.LaunchApp(t, ctx, appID)) + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "default", "") + + // Stop the fourth instance to make sure that there's no way we could + // still be running the polling loop before doing the below. + utiltest.TerminateApp(t, ctx, appID, instances[3]) + + // Verify that reaper picked up the previous instances and was watching + // instance[1] + expected = []device.InstanceState{device.InstanceStateRunning, device.InstanceStateNotRunning, device.InstanceStateRunning, device.InstanceStateDeleted} + for i, _ := range instances { + utiltest.VerifyState(t, ctx, expected[i], appID, instances[i]) + } + + utiltest.TerminateApp(t, ctx, appID, instances[2]) + + expected = []device.InstanceState{device.InstanceStateRunning, device.InstanceStateNotRunning, device.InstanceStateDeleted, device.InstanceStateDeleted} + for i, _ := range instances { + utiltest.VerifyState(t, ctx, expected[i], appID, instances[i]) + } + utiltest.TerminateApp(t, ctx, appID, instances[0]) + + dm.Terminate(os.Interrupt) + dm.S.Expect("dm terminated") + utiltest.VerifyNoRunningProcesses(t) +} diff --git a/x/ref/services/device/deviced/internal/impl/daemonreap/persistent_daemon_kill_test.go b/x/ref/services/device/deviced/internal/impl/daemonreap/persistent_daemon_kill_test.go new file mode 100644 index 000000000..95689491b --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/daemonreap/persistent_daemon_kill_test.go @@ -0,0 +1,80 @@ +// Copyright 2015 The Vanadium 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 daemonreap_test + +import ( + "os" + "syscall" + "testing" + "time" + + "v.io/v23/services/device" + "v.io/x/ref" + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" +) + +func TestReapRestartsDaemonMode(t *testing.T) { + cleanup, ctx, sh, envelope, root, helperPath, _ := utiltest.StartupHelper(t) + defer cleanup() + + // Start a device manager. + // (Since it will be restarted, use the VeyronCredentials environment + // to maintain the same set of credentials across runs) + dmCreds := utiltest.CreatePrincipal(t, sh) + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link") + dm.Vars[ref.EnvCredentials] = dmCreds + dm.Start() + dm.S.Expect("READY") + utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken) + + // Create the local server that the app uses to let us know it's ready. + pingCh, cleanup := utiltest.SetupPingServer(t, ctx) + defer cleanup() + utiltest.Resolve(t, ctx, "pingserver", 1, true) + + // Create an envelope for a daemon app. + *envelope = utiltest.EnvelopeFromShell(sh, nil, nil, utiltest.App, "google naps", 10, time.Hour, "appV1") + + // Install the app. + appID := utiltest.InstallApp(t, ctx) + + instance1 := utiltest.LaunchApp(t, ctx, appID) + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "default", "") + + // Get pid of first instance. + pid := utiltest.GetPid(t, ctx, appID, instance1) + + // Shutdown the first device manager. + dm.Terminate(os.Interrupt) + dm.S.Expect("dm terminated") + utiltest.ResolveExpectNotFound(t, ctx, "dm", false) // Ensure a clean slate. + + // Kill instance[0] and wait until it exits before proceeding. + syscall.Kill(pid, 9) + utiltest.PollingWait(t, int(pid)) + + // Run another device manager to replace the dead one. + dm = utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link") + dm.Vars[ref.EnvCredentials] = dmCreds + dm.Start() + + defer func() { + utiltest.TerminateApp(t, ctx, appID, instance1) + dm.Terminate(os.Interrupt) + dm.S.Expect("dm terminated") + utiltest.VerifyNoRunningProcesses(t) + }() + + dm.S.Expect("READY") + utiltest.Resolve(t, ctx, "dm", 1, true) // Verify the device manager has published itself. + + // The app will ping us. Wait for it. + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "default", "") + + // By now, we've reconciled the state of the tree with which processes + // are actually alive. instance1 was not alive but since it is configured as a + // daemon, it will have been restarted. + utiltest.WaitForState(t, ctx, device.InstanceStateRunning, appID, instance1) +} diff --git a/x/ref/services/device/deviced/internal/impl/device_service.go b/x/ref/services/device/deviced/internal/impl/device_service.go new file mode 100644 index 000000000..ff2ee1dad --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/device_service.go @@ -0,0 +1,587 @@ +// Copyright 2015 The Vanadium 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 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) +// logs/ - device manager logs +// STDERR-<timestamp> - one for each execution of device manager +// STDOUT-<timestamp> - one for each execution of device manager +// <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/ +// acls/ +// data +// signature +// associated.accounts +// persistent-args - list of persistent arguments for the device +// manager (json encoded) +// +// 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/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/services/binary" + "v.io/v23/services/device" + "v.io/v23/verror" + vexec "v.io/x/ref/lib/exec" + "v.io/x/ref/lib/mgmt" + "v.io/x/ref/services/device/internal/config" + "v.io/x/ref/services/device/internal/errors" + "v.io/x/ref/services/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 + restartHandler func() + callback *callbackState + config *config.State + disp *dispatcher + uat BlessingSystemAssociationStore + principalMgr principalManager + tidying chan<- tidyRequests +} + +// ManagerInfo holds state about a running device manager or a running restarter +type ManagerInfo struct { + Pid int +} + +func SaveManagerInfo(dir string, info *ManagerInfo) error { + jsonInfo, err := json.Marshal(info) + if err != nil { + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("Marshal(%v) failed: %v", info, err)) + } + if err := os.MkdirAll(dir, os.FileMode(0700)); err != nil { + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("MkdirAll(%v) failed: %v", dir, err)) + } + infoPath := filepath.Join(dir, "info") + if err := ioutil.WriteFile(infoPath, jsonInfo, 0600); err != nil { + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("WriteFile(%v) failed: %v", infoPath, err)) + } + return nil +} + +func LoadManagerInfo(dir string) (*ManagerInfo, error) { + infoPath := filepath.Join(dir, "info") + info := new(ManagerInfo) + if infoBytes, err := ioutil.ReadFile(infoPath); err != nil { + return nil, verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("ReadFile(%v) failed: %v", infoPath, err)) + } else if err := json.Unmarshal(infoBytes, info); err != nil { + return nil, verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("Unmarshal(%v) failed: %v", infoBytes, err)) + } + return info, nil +} + +func SavePersistentArgs(root string, args []string) error { + dir := filepath.Join(root, "device-manager", "device-data") + if err := os.MkdirAll(dir, 0700); err != nil { + return fmt.Errorf("MkdirAll(%q) failed: %v", dir, err) + } + data, err := json.Marshal(args) + if err != nil { + return fmt.Errorf("Marshal(%v) failed: %v", args, err) + } + fileName := filepath.Join(dir, "persistent-args") + return ioutil.WriteFile(fileName, data, 0600) +} + +func loadPersistentArgs(root string) ([]string, error) { + fileName := filepath.Join(root, "device-manager", "device-data", "persistent-args") + bytes, err := ioutil.ReadFile(fileName) + if err != nil { + return nil, err + } + args := []string{} + if err := json.Unmarshal(bytes, &args); err != nil { + return nil, fmt.Errorf("json.Unmarshal(%v) failed: %v", bytes, err) + } + return args, nil +} + +func (*deviceService) Describe(*context.T, rpc.ServerCall) (device.Description, error) { + return Describe() +} + +func (*deviceService) IsRunnable(_ *context.T, _ rpc.ServerCall, 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(_ *context.T, _ rpc.ServerCall, deadline time.Duration) 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 { + return nil, "", verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("Lstat(%v) failed: %v", path, err)) + } + scriptPath, err := filepath.EvalSymlinks(path) + if err != nil { + return nil, "", verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("EvalSymlinks(%v) failed: %v", path, 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 verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("UpdateLink failed: %v", err)) + } + if s.restartHandler != nil { + s.restartHandler() + } + v23.GetAppCycle(ctx).Stop(ctx) + 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 v23test/gosh +// 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) + cmd.Env = []string{"DEVICE_MANAGER_DONT_REDIRECT_STDOUT_STDERR=1"} + + 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(k string) { + if f, err := os.Open(fName); err == nil { + scanner := bufio.NewScanner(f) + for scanner.Scan() { + ctx.Infof("[testDeviceManager %s] %s", k, scanner.Text()) + } + } + }(k) + } + + // Setup up the child process callback. + callbackState := s.callback + listener := callbackState.listenFor(ctx, 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") + + principalMgr := s.principalMgr + if err := principalMgr.Create(workspace); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Create(%v) failed: %v", workspace, err)) + } + defer principalMgr.Delete(workspace) + if err := principalMgr.Serve(workspace, cfg); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Serve(%v) failed: %v", workspace, err)) + } + defer principalMgr.StopServing(workspace) + p, err := principalMgr.Load(workspace) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Load(%v) failed: %v", workspace, err)) + } + defer p.Close() + dmPrincipal := v23.GetPrincipal(ctx) + dmBlessings, _ := dmPrincipal.BlessingStore().Default() + testDmBlessings, err := dmPrincipal.Bless(p.PublicKey(), dmBlessings, "testdm", security.UnconstrainedUse()) + if err := p.BlessingStore().SetDefault(testDmBlessings); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("BlessingStore.SetDefault() failed: %v", err)) + } + if _, err := p.BlessingStore().Set(testDmBlessings, security.AllPrincipals); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("BlessingStore.Set() failed: %v", err)) + } + if err := security.AddToRoots(p, testDmBlessings); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("AddToRoots() failed: %v", err)) + } + + env, err := vexec.WriteConfigToEnv(cfg, cmd.Env) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("encoding config failed %v", err)) + } + cmd.Env = env + + if err := cmd.Start(); err != nil { + ctx.Errorf("Start() failed: %v", err) + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Start() failed: %v", err)) + } + + // Watch for the exit of the child. Failures could cause it to happen at any time + waitchan := make(chan error, 1) + go func() { + if err := cmd.Wait(); err != nil { + waitchan <- verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("new device manager failed to exit cleanly: %v", err)) + } + close(waitchan) + listener.stop() + }() + + childName, err := listener.waitForValue(childReadyTimeout) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("waitForValue(%v) failed: %v", childReadyTimeout, err)) + } + // Check that invoking Delete() succeeds. + childName = naming.Join(childName, "device") + dmClient := device.DeviceClient(childName) + if err := dmClient.Delete(ctx); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Delete() failed: %v", err)) + } + select { + case err := <-waitchan: + return err // err is nil if cmd.Wait succceeded + case <-time.After(childWaitTimeout): + return verror.New(errors.ErrOperationFailed, ctx, "new device manager failed to run in allotted time") + } + 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, logs string) 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 { + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("EvalSymlinks(%v) failed: %v", os.Args[0], err)) + } + + if err := os.MkdirAll(logs, 0711); err != nil { + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("MkdirAll(%v) failed: %v", logs, err)) + } + stderrLog, stdoutLog := filepath.Join(logs, "STDERR"), filepath.Join(logs, "STDOUT") + + output := "#!" + ShellPath + "\n" + output += "if [ -z \"$DEVICE_MANAGER_DONT_REDIRECT_STDOUT_STDERR\" ]; then\n" + output += fmt.Sprintf(" TIMESTAMP=$(%s)\n", DateCommand) + output += fmt.Sprintf(" exec > %s-$TIMESTAMP 2> %s-$TIMESTAMP\n", stdoutLog, stderrLog) + output += " LOG_TO_STDERR=false\n" + output += "else\n" + output += " LOG_TO_STDERR=true\n" + output += "fi\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 + // v.io/x/ref/services/debug/debug/impl.go) instead. + output += fmt.Sprintf("exec %q", filepath.Join(workspace, "deviced")) + " " + output += fmt.Sprintf("--log_dir=%q ", logs) + output += "--logtostderr=${LOG_TO_STDERR} " + output += strings.Join(envelope.Args, " ") + + path = filepath.Join(workspace, "deviced.sh") + if err := ioutil.WriteFile(path, []byte(output), 0700); err != nil { + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("WriteFile(%v) failed: %v", path, err)) + } + return nil +} + +func (s *deviceService) updateDeviceManager(ctx *context.T) error { + if len(s.config.Origin) == 0 { + return verror.New(errors.ErrUpdateNoOp, ctx) + } + envelope, err := fetchEnvelope(ctx, s.config.Origin) + if err != nil { + return err + } + if envelope.Title != application.DeviceManagerTitle { + return verror.New(errors.ErrAppTitleMismatch, ctx, fmt.Sprintf("app title mismatch. Got %q, expected %q.", envelope.Title, application.DeviceManagerTitle)) + } + // Read and merge persistent args, if present. + if args, err := loadPersistentArgs(s.config.Root); err == nil { + envelope.Args = append(envelope.Args, args...) + } + if s.config.Envelope != nil && reflect.DeepEqual(envelope, s.config.Envelope) { + return verror.New(errors.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 { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("MkdirAll(%v, %v) failed: %v", workspace, perm, err)) + } + + deferrer := func() { + CleanupDir(ctx, workspace, "") + } + defer func() { + if deferrer != nil { + deferrer() + } + }() + + // Populate the new workspace with a device manager binary. + // TODO(caprita): match identical binaries on binary signature + // rather than binary object name. + sameBinary := s.config.Envelope != nil && envelope.Binary.File == s.config.Envelope.Binary.File + if sameBinary { + if err := LinkSelf(workspace, "deviced"); err != nil { + return err + } + } else { + if err := downloadBinary(ctx, envelope.Publisher, &envelope.Binary, workspace, "deviced"); err != nil { + return err + } + } + + // Populate the new workspace with a device manager script. + configSettings, err := s.config.Save(envelope) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, err) + } + + logs := filepath.Join(s.config.Root, "device-manager", "logs") + if err := GenerateScript(workspace, configSettings, envelope, logs); err != nil { + return err + } + + if err := s.testDeviceManager(ctx, workspace, envelope); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("testDeviceManager failed: %v", err)) + } + + if err := UpdateLink(filepath.Join(workspace, "deviced.sh"), s.config.CurrentLink); err != nil { + return err + } + + if s.restartHandler != nil { + s.restartHandler() + } + v23.GetAppCycle(ctx).Stop(ctx) + deferrer = nil + return nil +} + +func (*deviceService) Install(ctx *context.T, _ rpc.ServerCall, _ string, _ device.Config, _ application.Packages) (string, error) { + return "", verror.New(errors.ErrInvalidSuffix, ctx) +} + +func (*deviceService) Run(ctx *context.T, _ rpc.ServerCall) error { + return verror.New(errors.ErrInvalidSuffix, ctx) +} + +func (s *deviceService) Revert(ctx *context.T, _ rpc.ServerCall) error { + if s.config.Previous == "" { + return verror.New(errors.ErrUpdateNoOp, ctx, fmt.Sprintf("Revert failed: no previous version")) + } + updatingState := s.updating + if updatingState.testAndSetUpdating() { + return verror.New(errors.ErrOperationInProgress, ctx, fmt.Sprintf("Revert failed: already in progress")) + } + err := s.revertDeviceManager(ctx) + if err != nil { + updatingState.unsetUpdating() + } + return err +} + +func (*deviceService) Instantiate(ctx *context.T, _ device.ApplicationInstantiateServerCall) (string, error) { + return "", verror.New(errors.ErrInvalidSuffix, ctx) +} + +func (*deviceService) Delete(ctx *context.T, _ rpc.ServerCall) error { + v23.GetAppCycle(ctx).Stop(ctx) + return nil +} + +func (s *deviceService) Kill(ctx *context.T, _ rpc.ServerCall, _ time.Duration) error { + if s.restartHandler != nil { + s.restartHandler() + } + v23.GetAppCycle(ctx).Stop(ctx) + return nil +} + +func (*deviceService) Uninstall(ctx *context.T, _ rpc.ServerCall) error { + return verror.New(errors.ErrInvalidSuffix, ctx) +} + +func (s *deviceService) Update(ctx *context.T, _ rpc.ServerCall) error { + ctx, cancel := context.WithTimeout(ctx, rpcContextLongTimeout) + defer cancel() + + updatingState := s.updating + if updatingState.testAndSetUpdating() { + return verror.New(errors.ErrOperationInProgress, ctx) + } + + err := s.updateDeviceManager(ctx) + if err != nil { + updatingState.unsetUpdating() + } + return err +} + +func (*deviceService) UpdateTo(*context.T, rpc.ServerCall, string) error { + // TODO(jsimsa): Implement. + return nil +} + +func (s *deviceService) SetPermissions(_ *context.T, _ rpc.ServerCall, perms access.Permissions, version string) error { + d := PermsDir(s.disp.config) + return s.disp.permsStore.Set(d, perms, version) +} + +func (s *deviceService) GetPermissions(*context.T, rpc.ServerCall) (perms access.Permissions, version string, err error) { + d := PermsDir(s.disp.config) + return s.disp.permsStore.Get(d) +} + +// TODO(rjkroege): Make it possible for users on the same system to also +// associate their accounts with their identities. +func (s *deviceService) AssociateAccount(_ *context.T, _ rpc.ServerCall, identityNames []string, accountName string) error { + if accountName == "" { + return s.uat.DisassociateSystemAccountForBlessings(identityNames) + } + // TODO(rjkroege): Optionally verify here that the required uname is a valid. + return s.uat.AssociateSystemAccountForBlessings(identityNames, accountName) +} + +func (s *deviceService) ListAssociations(ctx *context.T, call rpc.ServerCall) (associations []device.Association, err error) { + // Temporary code. Dump this. + if ctx.V(2) { + b, r := security.RemoteBlessingNames(ctx, call.Security()) + ctx.Infof("ListAssociations given blessings: %v\n", b) + if len(r) > 0 { + ctx.Infof("ListAssociations rejected blessings: %v\n", r) + } + } + return s.uat.AllBlessingSystemAssociations() +} + +func (*deviceService) Debug(*context.T, rpc.ServerCall) (string, error) { + return "Not implemented", nil +} + +func (s *deviceService) Status(*context.T, rpc.ServerCall) (device.Status, error) { + state := device.InstanceStateRunning + if s.updating.updating { + state = device.InstanceStateUpdating + } + // Extract the version from the current link path. + // + // TODO(caprita): make the version available in the device's directory. + scriptPath, err := filepath.EvalSymlinks(s.config.CurrentLink) + if err != nil { + return nil, err + } + dir := filepath.Dir(scriptPath) + versionDir := filepath.Base(dir) + if versionDir == "." { + versionDir = "base" + } + return device.StatusDevice{Value: device.DeviceStatus{ + State: state, + Version: versionDir, + }}, nil +} + +func (s *deviceService) TidyNow(ctx *context.T, _ rpc.ServerCall) error { + ec := make(chan error) + s.tidying <- tidyRequests{ctx: ctx, bc: ec} + return <-ec +} diff --git a/x/ref/services/device/deviced/internal/impl/dispatcher.go b/x/ref/services/device/deviced/internal/impl/dispatcher.go new file mode 100644 index 000000000..0c9da4692 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/dispatcher.go @@ -0,0 +1,377 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "fmt" + "path" + "path/filepath" + "strings" + "sync" + + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/device" + "v.io/v23/services/pprof" + libstats "v.io/v23/services/stats" + "v.io/v23/vdl" + "v.io/v23/vdlroot/signature" + "v.io/v23/verror" + s_device "v.io/x/ref/services/device" + "v.io/x/ref/services/device/internal/config" + "v.io/x/ref/services/device/internal/errors" + "v.io/x/ref/services/internal/logreaderlib" + "v.io/x/ref/services/internal/pathperms" +) + +// internalState wraps state shared between different device manager +// invocations. +type internalState struct { + callback *callbackState + updating *updatingState + principalMgr principalManager + restartHandler func() + stats *stats + testMode bool + // runner is responsible for running app instances. + runner *appRunner + // tidying is the automatic state tidying subsystem. + tidying chan<- tidyRequests +} + +// dispatcher holds the state of the device manager dispatcher. +type dispatcher struct { + // internal holds the state that persists across RPC method invocations. + internal *internalState + // config holds the device manager's (immutable) configuration state. + config *config.State + // dispatcherMutex is a lock for coordinating concurrent access to some + // dispatcher methods. + mu sync.RWMutex + // TODO(rjkroege): Consider moving this inside internal. + uat BlessingSystemAssociationStore + permsStore *pathperms.PathStore + // Namespace + mtAddress string // The address of the local mounttable. +} + +var _ rpc.Dispatcher = (*dispatcher)(nil) + +const ( + appsSuffix = "apps" + deviceSuffix = "device" + configSuffix = "cfg" + + // TODO(caprita): the value of pkgPath corresponds to the previous + // package where the error ids were defined. Updating error ids needs + // to be carefully coordinated between clients and servers, so we should + // do it when we settle on the final location for these error + // definitions. + pkgPath = "v.io/x/ref/services/device/internal/impl" +) + +var ( + errInvalidConfig = verror.Register(pkgPath+".errInvalidConfig", verror.NoRetry, "{1:}{2:} invalid config {3}{:_}") + errCantCreateAccountStore = verror.Register(pkgPath+".errCantCreateAccountStore", verror.NoRetry, "{1:}{2:} cannot create persistent store for identity to system account associations{:_}") + errCantCreateAppWatcher = verror.Register(pkgPath+".errCantCreateAppWatcher", verror.NoRetry, "{1:}{2:} cannot create app status watcher{:_}") + errNewAgentFailed = verror.Register(pkgPath+".errNewAgentFailed", verror.NoRetry, "{1:}{2:} NewAgent() failed{:_}") + errStoppedWithErrors = verror.Register(pkgPath+".errStoppedWithErrors", verror.NoRetry, "{1:}{2:} instance killed uncleanly{:_}") + errStopFailed = verror.Register(pkgPath+".errStopFailed", verror.NoRetry, "{1:}{2:} instance couldn't be killed{:_}") +) + +// NewDispatcher is the device manager dispatcher factory. It returns a new +// dispatcher as well as a shutdown function, to be called when the dispatcher +// is no longer needed. +func NewDispatcher(ctx *context.T, config *config.State, mtAddress string, testMode bool, restartHandler func(), permStore *pathperms.PathStore) (rpc.Dispatcher, func(), error) { + if err := config.Validate(); err != nil { + return nil, nil, verror.New(errInvalidConfig, ctx, config, err) + } + uat, err := NewBlessingSystemAssociationStore(config.Root) + if err != nil { + return nil, nil, verror.New(errCantCreateAccountStore, ctx, err) + } + InitSuidHelper(ctx, config.Helper) + d := &dispatcher{ + internal: &internalState{ + callback: newCallbackState(config.Name), + updating: newUpdatingState(), + restartHandler: restartHandler, + stats: newStats("device-manager"), + testMode: testMode, + tidying: newTidyingDaemon(ctx, config.Root), + principalMgr: newPrincipalManager(), + }, + config: config, + uat: uat, + permsStore: permStore, + mtAddress: mtAddress, + } + runner := &appRunner{ + callback: d.internal.callback, + principalMgr: d.internal.principalMgr, + appServiceName: naming.Join(d.config.Name, appsSuffix), + mtAddress: d.mtAddress, + stats: d.internal.stats, + } + d.internal.runner = runner + reap, err := newReaper(ctx, config.Root, runner) + if err != nil { + return nil, nil, verror.New(errCantCreateAppWatcher, ctx, err) + } + runner.reap = reap + + if testMode { + return &testModeDispatcher{d}, reap.shutdown, nil + } + return d, reap.shutdown, nil +} + +// Logging invoker that logs any error messages before returning. +func newLoggingInvoker(ctx *context.T, obj interface{}) (rpc.Invoker, error) { + if invoker, ok := obj.(rpc.Invoker); ok { + return &loggingInvoker{invoker: invoker}, nil + } + invoker, err := rpc.ReflectInvoker(obj) + if err != nil { + ctx.Errorf("rpc.ReflectInvoker returned error: %v", err) + return nil, err + } + return &loggingInvoker{invoker: invoker}, nil +} + +type loggingInvoker struct { + invoker rpc.Invoker +} + +func (l *loggingInvoker) Prepare(ctx *context.T, method string, numArgs int) (argptrs []interface{}, tags []*vdl.Value, err error) { + argptrs, tags, err = l.invoker.Prepare(ctx, method, numArgs) + if err != nil { + ctx.Errorf("Prepare(%s %d) returned error: %v", method, numArgs, err) + } + return +} + +func (l *loggingInvoker) Invoke(ctx *context.T, call rpc.StreamServerCall, method string, argptrs []interface{}) (results []interface{}, err error) { + results, err = l.invoker.Invoke(ctx, call, method, argptrs) + if err != nil { + ctx.Errorf("Invoke(method:%s argptrs:%v) returned error: %v", method, argptrs, err) + } + return +} + +func (l *loggingInvoker) Signature(ctx *context.T, call rpc.ServerCall) ([]signature.Interface, error) { + sig, err := l.invoker.Signature(ctx, call) + if err != nil { + ctx.Errorf("Signature returned error: %v", err) + } + return sig, err +} + +func (l *loggingInvoker) MethodSignature(ctx *context.T, call rpc.ServerCall, method string) (signature.Method, error) { + methodSig, err := l.invoker.MethodSignature(ctx, call, method) + if err != nil { + ctx.Errorf("MethodSignature(%s) returned error: %v", method, err) + } + return methodSig, err +} + +func (l *loggingInvoker) Globber() *rpc.GlobState { + return l.invoker.Globber() +} + +// DISPATCHER INTERFACE IMPLEMENTATION +func (d *dispatcher) Lookup(ctx *context.T, suffix string) (interface{}, security.Authorizer, error) { + invoker, auth, err := d.internalLookup(suffix) + if err != nil { + return nil, nil, err + } + loggingInvoker, err := newLoggingInvoker(ctx, invoker) + if err != nil { + return nil, nil, err + } + return loggingInvoker, auth, nil +} + +func newTestableHierarchicalAuth(testMode bool, rootDir, childDir string, get pathperms.PermsGetter) (security.Authorizer, error) { + if testMode { + // In test mode, the device manager will not be able to read the + // Permissions, because they were signed with the key of the real device + // manager. It's not a problem because the testModeDispatcher overrides the + // authorizer anyway. + return nil, nil + } + return pathperms.NewHierarchicalAuthorizer(rootDir, childDir, get) +} + +func (d *dispatcher) internalLookup(suffix string) (interface{}, security.Authorizer, error) { + components := strings.Split(suffix, "/") + for i := 0; i < len(components); i++ { + if len(components[i]) == 0 { + components = append(components[:i], components[i+1:]...) + i-- + } + } + + // TODO(rjkroege): Permit the root Permissions to diverge for the device and + // app sub-namespaces of the device manager after claiming. + auth, err := newTestableHierarchicalAuth(d.internal.testMode, PermsDir(d.config), PermsDir(d.config), d.permsStore) + if err != nil { + return nil, nil, err + } + + if len(components) == 0 { + return rpc.ChildrenGlobberInvoker(deviceSuffix, appsSuffix), auth, nil + } + // The implementation of the device manager is split up into several + // invokers, which are instantiated depending on the receiver name + // prefix. + switch components[0] { + case deviceSuffix: + receiver := device.DeviceServer(&deviceService{ + callback: d.internal.callback, + updating: d.internal.updating, + restartHandler: d.internal.restartHandler, + config: d.config, + disp: d, + uat: d.uat, + principalMgr: d.internal.principalMgr, + tidying: d.internal.tidying, + }) + return receiver, auth, nil + case appsSuffix: + // Requests to apps/*/*/*/logs are handled locally by LogFileService. + // Requests to apps/*/*/*/pprof are proxied to the apps' __debug/pprof object. + // Requests to apps/*/*/*/stats are proxied to the apps' __debug/stats object. + // Everything else is handled by the Application server. + if len(components) >= 5 { + appInstanceDir, err := instanceDir(d.config.Root, components[1:4]) + if err != nil { + return nil, nil, err + } + switch kind := components[4]; kind { + case "logs": + logsDir := filepath.Join(appInstanceDir, "logs") + suffix := naming.Join(components[5:]...) + appSpecificAuthorizer, err := newAppSpecificAuthorizer(auth, d.config, components[1:], d.permsStore) + if err != nil { + return nil, nil, err + } + return logreaderlib.NewLogFileService(logsDir, suffix), appSpecificAuthorizer, nil + case "pprof", "stats": + info, err := loadInstanceInfo(nil, appInstanceDir) + if err != nil { + return nil, nil, err + } + if !instanceStateIs(appInstanceDir, device.InstanceStateRunning) { + return nil, nil, verror.New(errors.ErrInvalidSuffix, nil) + } + var desc []rpc.InterfaceDesc + switch kind { + case "pprof": + desc = pprof.PProfServer(nil).Describe__() + case "stats": + desc = libstats.StatsServer(nil).Describe__() + } + suffix := naming.Join("__debug", naming.Join(components[4:]...)) + remote := naming.JoinAddressName(info.AppCycleMgrName, suffix) + + // Use hierarchical auth with debugacls under debug access. + appSpecificAuthorizer, err := newAppSpecificAuthorizer(auth, d.config, components[1:], d.permsStore) + if err != nil { + return nil, nil, err + } + return newProxyInvoker(remote, access.Debug, desc), appSpecificAuthorizer, nil + } + } + receiver := device.ApplicationServer(&appService{ + config: d.config, + suffix: components[1:], + uat: d.uat, + permsStore: d.permsStore, + runner: d.internal.runner, + stats: d.internal.stats, + }) + appSpecificAuthorizer, err := newAppSpecificAuthorizer(auth, d.config, components[1:], d.permsStore) + if err != nil { + return nil, nil, err + } + return receiver, appSpecificAuthorizer, nil + case configSuffix: + if len(components) != 2 { + return nil, nil, verror.New(errors.ErrInvalidSuffix, nil) + } + receiver := s_device.ConfigServer(&configService{ + callback: d.internal.callback, + suffix: components[1], + }) + // The nil authorizer ensures that only principals blessed by + // the device manager can talk back to it. All apps started by + // the device manager should fall in that category. + // + // TODO(caprita,rjkroege): We should further refine this, by + // only allowing the app to update state referring to itself + // (and not other apps). + return receiver, nil, nil + default: + return nil, nil, verror.New(errors.ErrInvalidSuffix, nil) + } +} + +// testModeDispatcher is a wrapper around the real dispatcher. It returns the +// exact same object as the real dispatcher, but the authorizer only allows +// calls to "device".Delete(). +type testModeDispatcher struct { + realDispatcher rpc.Dispatcher +} + +func (d *testModeDispatcher) Lookup(ctx *context.T, suffix string) (interface{}, security.Authorizer, error) { + obj, _, err := d.realDispatcher.Lookup(ctx, suffix) + return obj, d, err +} + +func (testModeDispatcher) Authorize(ctx *context.T, call security.Call) error { + if call.Suffix() == deviceSuffix && call.Method() == "Delete" { + ctx.Infof("testModeDispatcher.Authorize: Allow %q.%s()", call.Suffix(), call.Method()) + return nil + } + ctx.Infof("testModeDispatcher.Authorize: Reject %q.%s()", call.Suffix(), call.Method()) + return verror.New(errors.ErrInvalidSuffix, nil) +} + +func newAppSpecificAuthorizer(sec security.Authorizer, config *config.State, suffix []string, getter pathperms.PermsGetter) (security.Authorizer, error) { + // TODO(rjkroege): This does not support <appname>.Start() to start all + // instances. Correct this. + + // If we are attempting a method invocation against "apps/", we use the root + // Permissions. + if len(suffix) == 0 || len(suffix) == 1 { + return sec, nil + } + // Otherwise, we require a per-installation and per-instance Permissions file. + if len(suffix) == 2 { + p, err := installationDirCore(suffix, config.Root) + if err != nil { + return nil, verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("newAppSpecificAuthorizer failed: %v", err)) + } + return pathperms.NewHierarchicalAuthorizer(PermsDir(config), path.Join(p, "acls"), getter) + } + // Use the special debugacls for instance/logs, instance/pprof, instance/stats. + if len(suffix) > 3 && (suffix[3] == "logs" || suffix[3] == "pprof" || suffix[3] == "stats") { + p, err := instanceDir(config.Root, suffix[0:3]) + if err != nil { + return nil, verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("newAppSpecificAuthorizer failed: %v", err)) + } + return pathperms.NewHierarchicalAuthorizer(PermsDir(config), path.Join(p, "debugacls"), getter) + } + + p, err := instanceDir(config.Root, suffix[0:3]) + if err != nil { + return nil, verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("newAppSpecificAuthorizer failed: %v", err)) + } + return pathperms.NewHierarchicalAuthorizer(PermsDir(config), path.Join(p, "acls"), getter) +} diff --git a/x/ref/services/device/deviced/internal/impl/globsuid/args_darwin_test.go b/x/ref/services/device/deviced/internal/impl/globsuid/args_darwin_test.go new file mode 100644 index 000000000..89252951c --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/globsuid/args_darwin_test.go @@ -0,0 +1,10 @@ +// Copyright 2015 The Vanadium 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 globsuid_test + +const ( + testUserName = "_uucp" + anotherTestUserName = "_lp" +) diff --git a/x/ref/services/device/deviced/internal/impl/globsuid/args_linux_test.go b/x/ref/services/device/deviced/internal/impl/globsuid/args_linux_test.go new file mode 100644 index 000000000..bafa34f81 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/globsuid/args_linux_test.go @@ -0,0 +1,10 @@ +// Copyright 2015 The Vanadium 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 globsuid_test + +const ( + testUserName = "daemon" + anotherTestUserName = "nobody" +) diff --git a/x/ref/services/device/deviced/internal/impl/globsuid/glob_test.go b/x/ref/services/device/deviced/internal/impl/globsuid/glob_test.go new file mode 100644 index 000000000..b115e82bf --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/globsuid/glob_test.go @@ -0,0 +1,112 @@ +// Copyright 2015 The Vanadium 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 globsuid_test + +import ( + "path" + "testing" + + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" + "v.io/x/ref/services/device/deviced/internal/versioning" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/test" +) + +func TestDeviceManagerGlobAndDebug(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + sh, deferFn := servicetest.CreateShellAndMountTable(t, ctx) + defer deferFn() + + // Set up mock application and binary repositories. + envelope, cleanup := utiltest.StartMockRepos(t, ctx) + defer cleanup() + + root, cleanup := servicetest.SetupRootDir(t, "devicemanager") + defer cleanup() + if err := versioning.SaveCreatorInfo(ctx, root); err != nil { + t.Fatal(err) + } + + // Create a script wrapping the test target that implements suidhelper. + helperPath := utiltest.GenerateSuidHelperScript(t, root) + + // Set up the device manager. Since we won't do device manager updates, + // don't worry about its application envelope and current link. + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link") + dm.Start() + dm.S.Expect("READY") + + // Create the local server that the app uses to let us know it's ready. + pingCh, cleanup := utiltest.SetupPingServer(t, ctx) + defer cleanup() + + // Create the envelope for the first version of the app. + *envelope = utiltest.EnvelopeFromShell(sh, nil, nil, utiltest.App, "google naps", 0, 0, "appV1") + + // Device must be claimed before applications can be installed. + utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken) + // Install the app. + appID := utiltest.InstallApp(t, ctx) + install1ID := path.Base(appID) + + // Start an instance of the app. + instance1ID := utiltest.LaunchApp(t, ctx, appID) + defer utiltest.TerminateApp(t, ctx, appID, instance1ID) + + // Wait until the app pings us that it's ready. + pingCh.WaitForPingArgs(t) + + app2ID := utiltest.InstallApp(t, ctx) + install2ID := path.Base(app2ID) + + // Base name of argv[0] that the app should have when it executes + // It will be path.Base(envelope.Title + "@" + envelope.Binary.File + "/app"). + // Note the suffix, which ensures that the result is always "app" at the moment. + // Someday in future we may remove that and have binary names that reflect the app name. + const appName = "app" + + testcases := []utiltest.GlobTestVector{ + {"dm", "...", []string{ + "", + "apps", + "apps/google naps", + "apps/google naps/" + install1ID, + "apps/google naps/" + install1ID + "/" + instance1ID, + "apps/google naps/" + install1ID + "/" + instance1ID + "/logs", + "apps/google naps/" + install1ID + "/" + instance1ID + "/logs/STDERR-<timestamp>", + "apps/google naps/" + install1ID + "/" + instance1ID + "/logs/STDOUT-<timestamp>", + "apps/google naps/" + install1ID + "/" + instance1ID + "/logs/" + appName + ".INFO", + "apps/google naps/" + install1ID + "/" + instance1ID + "/logs/" + appName + ".<*>.INFO.<timestamp>", + "apps/google naps/" + install1ID + "/" + instance1ID + "/pprof", + "apps/google naps/" + install1ID + "/" + instance1ID + "/stats", + "apps/google naps/" + install1ID + "/" + instance1ID + "/stats/rpc", + "apps/google naps/" + install1ID + "/" + instance1ID + "/stats/system", + "apps/google naps/" + install1ID + "/" + instance1ID + "/stats/system/start-time-rfc1123", + "apps/google naps/" + install1ID + "/" + instance1ID + "/stats/system/start-time-unix", + "apps/google naps/" + install2ID, + "device", + }}, + {"dm/apps", "*", []string{"google naps"}}, + {"dm/apps/google naps", "*", []string{install1ID, install2ID}}, + {"dm/apps/google naps/" + install1ID, "*", []string{instance1ID}}, + {"dm/apps/google naps/" + install1ID + "/" + instance1ID, "*", []string{"logs", "pprof", "stats"}}, + {"dm/apps/google naps/" + install1ID + "/" + instance1ID + "/logs", "*", []string{ + "STDERR-<timestamp>", + "STDOUT-<timestamp>", + appName + ".INFO", + appName + ".<*>.INFO.<timestamp>", + }}, + {"dm/apps/google naps/" + install1ID + "/" + instance1ID + "/stats/system", "start-time*", []string{"start-time-rfc1123", "start-time-unix"}}, + } + + res := utiltest.NewGlobTestRegexHelper(appName) + + utiltest.VerifyGlob(t, ctx, appName, testcases, res) + utiltest.VerifyLog(t, ctx, "dm", "apps/google naps", install1ID, instance1ID, "logs", "*") + utiltest.VerifyStatsValues(t, ctx, "dm", "apps/google naps", install1ID, instance1ID, "stats/system/start-time*") + utiltest.VerifyPProfCmdLine(t, ctx, appName, "dm", "apps/google naps", install1ID, instance1ID, "pprof") +} diff --git a/x/ref/services/device/deviced/internal/impl/globsuid/impl_test.go b/x/ref/services/device/deviced/internal/impl/globsuid/impl_test.go new file mode 100644 index 000000000..783a31f13 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/globsuid/impl_test.go @@ -0,0 +1,19 @@ +// Copyright 2015 The Vanadium 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 globsuid_test + +import ( + "testing" + + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" +) + +func TestMain(m *testing.M) { + utiltest.TestMainImpl(m) +} + +func TestSuidHelper(t *testing.T) { + utiltest.TestSuidHelperImpl(t) +} diff --git a/x/ref/services/device/deviced/internal/impl/globsuid/signature_match_test.go b/x/ref/services/device/deviced/internal/impl/globsuid/signature_match_test.go new file mode 100644 index 000000000..e6780be03 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/globsuid/signature_match_test.go @@ -0,0 +1,161 @@ +// Copyright 2015 The Vanadium 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 globsuid_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "v.io/v23" + "v.io/v23/naming" + "v.io/v23/security" + "v.io/v23/services/application" + "v.io/v23/services/device" + "v.io/v23/services/repository" + "v.io/v23/verror" + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" + "v.io/x/ref/services/device/deviced/internal/versioning" + "v.io/x/ref/services/device/internal/errors" + "v.io/x/ref/services/internal/binarylib" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" +) + +func TestDownloadSignatureMatch(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + rg := testutil.NewRandGenerator(t.Logf) + + sh, deferFn := servicetest.CreateShellAndMountTable(t, ctx) + defer deferFn() + + binaryVON := "binary" + pkgVON := naming.Join(binaryVON, "testpkg") + defer utiltest.StartRealBinaryRepository(t, ctx, binaryVON)() + + up := rg.RandomBytes(rg.RandomIntn(5 << 20)) + mediaInfo := repository.MediaInfo{Type: "application/octet-stream"} + sig, err := binarylib.Upload(ctx, naming.Join(binaryVON, "testbinary"), up, mediaInfo) + if err != nil { + t.Fatalf("Upload(%v) failed:%v", binaryVON, err) + } + + // Upload packages for this application + tmpdir, err := ioutil.TempDir("", "test-package-") + if err != nil { + t.Fatalf("ioutil.TempDir failed: %v", err) + } + defer os.RemoveAll(tmpdir) + pkgContents := rg.RandomBytes(rg.RandomIntn(5 << 20)) + if err := ioutil.WriteFile(filepath.Join(tmpdir, "pkg.txt"), pkgContents, 0600); err != nil { + t.Fatalf("ioutil.WriteFile failed: %v", err) + } + pkgSig, err := binarylib.UploadFromDir(ctx, pkgVON, tmpdir) + if err != nil { + t.Fatalf("binarylib.UploadFromDir failed: %v", err) + } + + // Start the application repository + envelope, serverStop := utiltest.StartApplicationRepository(ctx) + defer serverStop() + + root, cleanup := servicetest.SetupRootDir(t, "devicemanager") + defer cleanup() + if err := versioning.SaveCreatorInfo(ctx, root); err != nil { + t.Fatal(err) + } + + // Create a script wrapping the test target that implements suidhelper. + helperPath := utiltest.GenerateSuidHelperScript(t, root) + + // Set up the device manager. Since we won't do device manager updates, + // don't worry about its application envelope and current link. + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link") + dm.Start() + dm.S.Expect("READY") + utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken) + + p := v23.GetPrincipal(ctx) + publisher, err := p.BlessSelf("publisher") + if err != nil { + t.Fatalf("Failed to generate publisher blessings:%v", err) + } + *envelope = application.Envelope{ + Binary: application.SignedFile{ + File: naming.Join(binaryVON, "testbinary"), + Signature: *sig, + }, + Publisher: publisher, + Packages: map[string]application.SignedFile{ + "pkg": application.SignedFile{ + File: pkgVON, + Signature: *pkgSig, + }, + }, + } + + // Using the publisher should fail, because blessing "publisher" is not covered by the + // trusted roots of the device manager's principal + if _, err := utiltest.AppStub().Install(ctx, utiltest.MockApplicationRepoName, device.Config{}, nil); verror.ErrorID(err) != errors.ErrOperationFailed.ID { + t.Fatalf("Unexpected error installing app:%v (expected ErrOperationFailed)", err) + } + + // Changing the publisher blessing to one that is covered by the DM roots, should + // allow the app installation to succeed. + b, _ := p.BlessingStore().Default() + envelope.Publisher, err = p.Bless(p.PublicKey(), b, "publisher", security.UnconstrainedUse()) + if err != nil { + t.Fatalf("Failed to generate trusted publisher blessings: %v", err) + } + + if _, err := utiltest.AppStub().Install(ctx, utiltest.MockApplicationRepoName, device.Config{}, nil); err != nil { + t.Fatalf("Failed to Install app:%v", err) + } + + // Verify that when the binary is corrupted, signature verification fails. + up[0] = up[0] ^ 0xFF + if err := binarylib.Delete(ctx, naming.Join(binaryVON, "testbinary")); err != nil { + t.Fatalf("Delete(%v) failed:%v", binaryVON, err) + } + if _, err := binarylib.Upload(ctx, naming.Join(binaryVON, "testbinary"), up, mediaInfo); err != nil { + t.Fatalf("Upload(%v) failed:%v", binaryVON, err) + } + if _, err := utiltest.AppStub().Install(ctx, utiltest.MockApplicationRepoName, device.Config{}, nil); verror.ErrorID(err) != errors.ErrOperationFailed.ID { + t.Fatalf("Failed to verify signature mismatch for binary:%v. Got errorid=%v[%v], want errorid=%v", binaryVON, verror.ErrorID(err), err, errors.ErrOperationFailed.ID) + } + + // Restore the binary and verify that installation succeeds. + up[0] = up[0] ^ 0xFF + if err := binarylib.Delete(ctx, naming.Join(binaryVON, "testbinary")); err != nil { + t.Fatalf("Delete(%v) failed:%v", binaryVON, err) + } + if _, err := binarylib.Upload(ctx, naming.Join(binaryVON, "testbinary"), up, mediaInfo); err != nil { + t.Fatalf("Upload(%v) failed:%v", binaryVON, err) + } + if _, err := utiltest.AppStub().Install(ctx, utiltest.MockApplicationRepoName, device.Config{}, nil); err != nil { + t.Fatalf("Failed to Install app:%v", err) + } + + // Verify that when the package contents are corrupted, signature verification fails. + pkgContents[0] = pkgContents[0] ^ 0xFF + if err := binarylib.Delete(ctx, pkgVON); err != nil { + t.Fatalf("Delete(%v) failed:%v", pkgVON, err) + } + if err := os.Remove(filepath.Join(tmpdir, "pkg.txt")); err != nil { + t.Fatalf("Remove(%v) failed:%v", filepath.Join(tmpdir, "pkg.txt"), err) + } + if err := ioutil.WriteFile(filepath.Join(tmpdir, "pkg.txt"), pkgContents, 0600); err != nil { + t.Fatalf("ioutil.WriteFile failed: %v", err) + } + if _, err = binarylib.UploadFromDir(ctx, pkgVON, tmpdir); err != nil { + t.Fatalf("binarylib.UploadFromDir failed: %v", err) + } + if _, err := utiltest.AppStub().Install(ctx, utiltest.MockApplicationRepoName, device.Config{}, nil); verror.ErrorID(err) != errors.ErrOperationFailed.ID { + t.Fatalf("Failed to verify signature mismatch for package:%v", pkgVON) + } +} diff --git a/x/ref/services/device/deviced/internal/impl/globsuid/suid_test.go b/x/ref/services/device/deviced/internal/impl/globsuid/suid_test.go new file mode 100644 index 000000000..cf7fd87ca --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/globsuid/suid_test.go @@ -0,0 +1,203 @@ +// Copyright 2015 The Vanadium 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 globsuid_test + +import ( + "flag" + "fmt" + "os" + "reflect" + "testing" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/device" + "v.io/v23/verror" + "v.io/x/ref/services/device/deviced/internal/impl" + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" + "v.io/x/ref/services/device/deviced/internal/versioning" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" +) + +var mockIsSetuid = flag.Bool("mocksetuid", false, "set flag to pretend to have a helper with setuid permissions") + +func possiblyMockIsSetuid(ctx *context.T, fileStat os.FileInfo) bool { + ctx.VI(2).Infof("Mock isSetuid is reporting: %v", *mockIsSetuid) + return *mockIsSetuid +} + +func init() { + impl.IsSetuid = possiblyMockIsSetuid +} + +func TestAppWithSuidHelper(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + // Use a common root identity provider so that all principals can talk to one + // another. + idp := testutil.NewIDProvider("root") + if err := idp.Bless(v23.GetPrincipal(ctx), "self"); err != nil { + t.Fatal(err) + } + + sh, deferFn := servicetest.CreateShellAndMountTable(t, ctx) + defer deferFn() + + // Set up mock application and binary repositories. + envelope, cleanup := utiltest.StartMockRepos(t, ctx) + defer cleanup() + + root, cleanup := servicetest.SetupRootDir(t, "devicemanager") + defer cleanup() + if err := versioning.SaveCreatorInfo(ctx, root); err != nil { + t.Fatal(err) + } + + selfCtx := ctx + otherCtx := utiltest.CtxWithNewPrincipal(t, selfCtx, idp, "other") + + // Create a script wrapping the test target that implements suidhelper. + helperPath := utiltest.GenerateSuidHelperScript(t, root) + + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link") + dm.Args = append(dm.Args, "-mocksetuid") + dm.Start() + dm.S.Expect("READY") + defer utiltest.VerifyNoRunningProcesses(t) + // Claim the devicemanager with selfCtx as root:self:alice + utiltest.ClaimDevice(t, selfCtx, "claimable", "dm", "alice", utiltest.NoPairingToken) + + deviceStub := device.DeviceClient("dm/device") + + // Create the local server that the app uses to tell us which system + // name the device manager wished to run it as. + pingCh, cleanup := utiltest.SetupPingServer(t, ctx) + defer cleanup() + + // Create an envelope for a first version of the app. + *envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-var"}, []string{fmt.Sprintf("--%s=flag-val-envelope", utiltest.TestFlagName)}, utiltest.App, "google naps", 0, 0, "appV1") + + // Install and start the app as root:self. + appID := utiltest.InstallApp(t, selfCtx) + + ctx.VI(2).Infof("Validate that the created app has the right permission lists.") + perms, _, err := utiltest.AppStub(appID).GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions on appID: %v failed %v", appID, err) + } + expected := make(access.Permissions) + for _, tag := range access.AllTypicalTags() { + expected[string(tag)] = access.AccessList{In: []security.BlessingPattern{"root:self:$"}} + } + if got, want := perms.Normalize(), expected.Normalize(); !reflect.DeepEqual(got, want) { + t.Errorf("got %#v, expected %#v", got, want) + } + + // Start an instance of the app but this time it should fail: we do not + // have an associated uname for the invoking identity. + utiltest.LaunchAppExpectError(t, selfCtx, appID, verror.ErrNoAccess.ID) + + // Create an association for selfCtx + if err := deviceStub.AssociateAccount(selfCtx, []string{"root:self"}, testUserName); err != nil { + t.Fatalf("AssociateAccount failed %v", err) + } + + instance1ID := utiltest.LaunchApp(t, selfCtx, appID) + pingCh.VerifyPingArgs(t, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready. + utiltest.TerminateApp(t, selfCtx, appID, instance1ID) + + ctx.VI(2).Infof("other attempting to run an app without access. Should fail.") + utiltest.LaunchAppExpectError(t, otherCtx, appID, verror.ErrNoAccess.ID) + + // Self will now let other also install apps. + if err := deviceStub.AssociateAccount(selfCtx, []string{"root:other"}, testUserName); err != nil { + t.Fatalf("AssociateAccount failed %v", err) + } + // Add Start to the AccessList list for root:other. + newAccessList, _, err := deviceStub.GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions failed %v", err) + } + newAccessList.Add("root:other", string(access.Write)) + if err := deviceStub.SetPermissions(selfCtx, newAccessList, ""); err != nil { + t.Fatalf("SetPermissions failed %v", err) + } + + // With the introduction of per installation and per instance AccessLists, + // while other now has administrator permissions on the device manager, + // other doesn't have execution permissions for the app. So this will + // fail. + ctx.VI(2).Infof("other attempting to run an app still without access. Should fail.") + utiltest.LaunchAppExpectError(t, otherCtx, appID, verror.ErrNoAccess.ID) + + // But self can give other permissions to start applications. + ctx.VI(2).Infof("self attempting to give other permission to start %s", appID) + newAccessList, _, err = utiltest.AppStub(appID).GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions on appID: %v failed %v", appID, err) + } + newAccessList.Add("root:other", string(access.Read)) + if err = utiltest.AppStub(appID).SetPermissions(selfCtx, newAccessList, ""); err != nil { + t.Fatalf("SetPermissions on appID: %v failed: %v", appID, err) + } + + ctx.VI(2).Infof("other attempting to run an app with access. Should succeed.") + instance2ID := utiltest.LaunchApp(t, otherCtx, appID) + pingCh.VerifyPingArgs(t, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready. + + ctx.VI(2).Infof("Validate that created instance has the right permissions.") + expected = make(access.Permissions) + for _, tag := range access.AllTypicalTags() { + expected[string(tag)] = access.AccessList{In: []security.BlessingPattern{"root:other:$"}} + } + perms, _, err = utiltest.AppStub(appID, instance2ID).GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions on instance %v/%v failed: %v", appID, instance2ID, err) + } + if got, want := perms.Normalize(), expected.Normalize(); !reflect.DeepEqual(got, want) { + t.Errorf("got %#v, expected %#v ", got, want) + } + + // Shutdown the app. + utiltest.KillApp(t, otherCtx, appID, instance2ID) + + ctx.VI(2).Infof("Verify that Run with the same systemName works.") + utiltest.RunApp(t, otherCtx, appID, instance2ID) + pingCh.VerifyPingArgs(t, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready. + utiltest.KillApp(t, otherCtx, appID, instance2ID) + + ctx.VI(2).Infof("Verify that other can install and run applications.") + otherAppID := utiltest.InstallApp(t, otherCtx) + + ctx.VI(2).Infof("other attempting to run an app that other installed. Should succeed.") + instance4ID := utiltest.LaunchApp(t, otherCtx, otherAppID) + pingCh.VerifyPingArgs(t, testUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready. + + // Clean up. + utiltest.TerminateApp(t, otherCtx, otherAppID, instance4ID) + + // Change the associated system name. + if err := deviceStub.AssociateAccount(selfCtx, []string{"root:other"}, anotherTestUserName); err != nil { + t.Fatalf("AssociateAccount failed %v", err) + } + + ctx.VI(2).Infof("Show that Run with a different systemName fails.") + utiltest.RunAppExpectError(t, otherCtx, appID, instance2ID, verror.ErrNoAccess.ID) + + // Clean up. + utiltest.DeleteApp(t, otherCtx, appID, instance2ID) + + ctx.VI(2).Infof("Show that Start with different systemName works.") + instance3ID := utiltest.LaunchApp(t, otherCtx, appID) + pingCh.VerifyPingArgs(t, anotherTestUserName, "flag-val-envelope", "env-var") // Wait until the app pings us that it's ready. + + // Clean up. + utiltest.TerminateApp(t, otherCtx, appID, instance3ID) +} diff --git a/x/ref/services/device/deviced/internal/impl/helper_manager.go b/x/ref/services/device/deviced/internal/impl/helper_manager.go new file mode 100644 index 000000000..961b89f16 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/helper_manager.go @@ -0,0 +1,200 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "os/user" + "strconv" + + "v.io/v23/context" + "v.io/v23/security" + "v.io/v23/verror" + "v.io/x/ref/services/device/internal/errors" +) + +type suidHelperState struct { + dmUser string // user that the device manager is running as + helperPath string // path to the suidhelper binary +} + +var suidHelper *suidHelperState + +func InitSuidHelper(ctx *context.T, helperPath string) { + if suidHelper != nil { + return + } + if helperPath == "" { + ctx.Panicf("suidhelper path needs to be specified") + } + + var userName string + if user, _ := user.Current(); user != nil && len(user.Username) > 0 { + userName = user.Username + } else { + userName = "anonymous" + } + suidHelper = &suidHelperState{ + dmUser: userName, + helperPath: helperPath, + } +} + +func (s suidHelperState) getCurrentUser() string { + return s.dmUser +} + +// terminatePid sends a SIGKILL to the target pid +func (s suidHelperState) terminatePid(ctx *context.T, pid int, stdout, stderr io.Writer) error { + if err := s.internalModalOp(ctx, stdout, stderr, "--kill", strconv.Itoa(pid)); err != nil { + return fmt.Errorf("devicemanager's invocation of suidhelper to kill pid %v failed: %v", pid, err) + } + return nil +} + +func DeleteFileTree(ctx *context.T, dirOrFile string, stdout, stderr io.Writer) error { + return suidHelper.deleteFileTree(ctx, dirOrFile, stdout, stderr) +} + +// deleteFileTree deletes a file or directory +func (s suidHelperState) deleteFileTree(ctx *context.T, dirOrFile string, stdout, stderr io.Writer) error { + if err := s.internalModalOp(ctx, stdout, stderr, "--rm", dirOrFile); err != nil { + return fmt.Errorf("devicemanager's invocation of suidhelper delete %v failed: %v", dirOrFile, err) + } + return nil +} + +// chown files or directories +func (s suidHelperState) chownTree(ctx *context.T, username string, dirOrFile string, stdout, stderr io.Writer) error { + args := []string{"--chown", "--username", username, dirOrFile} + + if err := s.internalModalOp(ctx, stdout, stderr, args...); err != nil { + return fmt.Errorf("devicemanager's invocation of suidhelper chown %v failed: %v", dirOrFile, err) + } + return nil +} + +type suidAppCmdArgs struct { + // args to helper + targetUser, progname, workspace, logdir, binpath, sockPath string + // fields in exec.Cmd + env []string + stdout, stderr io.Writer + dir string + // arguments passed to app + appArgs []string +} + +// getAppCmd produces an exec.Cmd that can be used to start an app +func (s suidHelperState) getAppCmd(ctx *context.T, a *suidAppCmdArgs) (*exec.Cmd, error) { + if a.targetUser == "" || a.progname == "" || a.binpath == "" || a.workspace == "" || a.logdir == "" { + return nil, fmt.Errorf("Invalid args passed to getAppCmd: %+v", a) + } + + cmd := exec.Command(s.helperPath) + + switch yes, err := s.suidhelperEnabled(ctx, a.targetUser); { + case err != nil: + return nil, err + case yes: + cmd.Args = append(cmd.Args, "--username", a.targetUser) + case !yes: + cmd.Args = append(cmd.Args, "--username", a.targetUser, "--dryrun") + } + + cmd.Args = append(cmd.Args, "--progname", a.progname) + cmd.Args = append(cmd.Args, "--workspace", a.workspace) + cmd.Args = append(cmd.Args, "--logdir", a.logdir) + if a.sockPath != "" { + cmd.Args = append(cmd.Args, "--agentsock", a.sockPath) + } + + cmd.Args = append(cmd.Args, "--run", a.binpath) + cmd.Args = append(cmd.Args, "--") + + cmd.Args = append(cmd.Args, a.appArgs...) + + cmd.Env = a.env + cmd.Stdout = a.stdout + cmd.Stderr = a.stderr + cmd.Dir = a.dir + + return cmd, nil +} + +// internalModalOp is a convenience routine containing the common part of all +// modal operations. Only other routines implementing specific suidhelper +// operations (like terminatePid and deleteFileTree) should call this directly. +func (s suidHelperState) internalModalOp(ctx *context.T, stdout, stderr io.Writer, arg ...string) error { + var captureStdout, captureStderr bytes.Buffer + stdoutWriters := []io.Writer{&captureStdout} + stderrWriters := []io.Writer{&captureStderr} + if stdout != nil { + stdoutWriters = append(stdoutWriters, stdout) + } + if stderr != nil { + stderrWriters = append(stderrWriters, stderr) + } + + cmd := exec.Command(s.helperPath) + cmd.Args = append(cmd.Args, arg...) + cmd.Stdout = io.MultiWriter(stdoutWriters...) + cmd.Stderr = io.MultiWriter(stderrWriters...) + + if err := cmd.Run(); err != nil { + ctx.Errorf("failed calling helper with args (%v): %v", arg, err) + ctx.Errorf("stdout: %s", captureStdout.String()) + ctx.Errorf("stderr: %s", captureStderr.String()) + return err + } + return nil +} + +// IsSetuid is defined like this so we can override its +// implementation for tests. +var IsSetuid = func(ctx *context.T, fileStat os.FileInfo) bool { + ctx.VI(2).Infof("running the original isSetuid") + return fileStat.Mode()&os.ModeSetuid == os.ModeSetuid +} + +// suidhelperEnabled determines if the suidhelper must exist and be +// setuid to run an application as system user targetUser. If false, the +// setuidhelper must be invoked with the --dryrun flag to skip making +// system calls that will fail or provide apps with a trivial +// priviledge escalation. +func (s suidHelperState) suidhelperEnabled(ctx *context.T, targetUser string) (bool, error) { + helperStat, err := os.Stat(s.helperPath) + if err != nil { + return false, verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("Stat(%v) failed: %v. helper is required.", s.helperPath, err)) + } + haveHelper := IsSetuid(ctx, helperStat) + + switch { + case haveHelper && s.dmUser != targetUser: + return true, nil + case haveHelper && s.dmUser == targetUser: + return false, verror.New(verror.ErrNoAccess, nil, fmt.Sprintf("suidhelperEnabled failed: %q == %q", s.dmUser, targetUser)) + default: + return false, nil + } +} + +// usernameForPrincipal returns the system name that the +// devicemanager will use to invoke apps. +// TODO(rjkroege): This code assumes a desktop target and will need +// to be reconsidered for embedded contexts. +func (s suidHelperState) usernameForPrincipal(ctx *context.T, call security.Call, uat BlessingSystemAssociationStore) string { + identityNames, _ := security.RemoteBlessingNames(ctx, call) + systemName, present := uat.SystemAccountForBlessings(identityNames) + if present { + return systemName + } else { + return s.dmUser + } +} diff --git a/x/ref/services/device/deviced/internal/impl/impl_helper_test.go b/x/ref/services/device/deviced/internal/impl/impl_helper_test.go new file mode 100644 index 000000000..934d31d37 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/impl_helper_test.go @@ -0,0 +1,55 @@ +// Copyright 2015 The Vanadium 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 impl_test + +// Separate from impl_test to avoid contributing further to impl_test bloat. +// TODO(rjkroege): Move all helper-related tests to here. + +import ( + "io/ioutil" + "os" + "path" + "testing" + + "v.io/v23/context" + "v.io/x/ref/internal/logger" + "v.io/x/ref/services/device/deviced/internal/impl" + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" +) + +func TestBaseCleanupDir(t *testing.T) { + ctx, cancel := context.RootContext() + defer cancel() + ctx = context.WithLogger(ctx, logger.Global()) + dir, err := ioutil.TempDir("", "impl_helper_test") + if err != nil { + t.Fatalf("ioutil.TempDir() failed: %v", err) + } + defer os.RemoveAll(dir) + + // Setup some files to delete. + helperTarget := path.Join(dir, "helper_target") + if err := os.MkdirAll(helperTarget, os.FileMode(0700)); err != nil { + t.Fatalf("os.MkdirAll(%s) failed: %v", helperTarget, err) + } + + nohelperTarget := path.Join(dir, "nohelper_target") + if err := os.MkdirAll(nohelperTarget, os.FileMode(0700)); err != nil { + t.Fatalf("os.MkdirAll(%s) failed: %v", nohelperTarget, err) + } + + // Setup a helper. + helper := utiltest.GenerateSuidHelperScript(t, dir) + + impl.BaseCleanupDir(ctx, helperTarget, helper) + if _, err := os.Stat(helperTarget); err == nil || os.IsExist(err) { + t.Fatalf("%s should be missing but isn't", helperTarget) + } + + impl.BaseCleanupDir(ctx, nohelperTarget, "") + if _, err := os.Stat(nohelperTarget); err == nil || os.IsExist(err) { + t.Fatalf("%s should be missing but isn't", nohelperTarget) + } +} diff --git a/x/ref/services/device/deviced/internal/impl/impl_test.go b/x/ref/services/device/deviced/internal/impl/impl_test.go new file mode 100644 index 000000000..4ede95256 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/impl_test.go @@ -0,0 +1,598 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// TODO(rjkroege): Add a more extensive unit test case to exercise AccessList +// logic. + +package impl_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/services/application" + "v.io/v23/services/device" + "v.io/x/lib/envvar" + "v.io/x/ref" + "v.io/x/ref/services/device/deviced/internal/impl" + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" + "v.io/x/ref/services/device/deviced/internal/installer" + "v.io/x/ref/services/device/deviced/internal/versioning" + "v.io/x/ref/services/device/internal/config" + "v.io/x/ref/services/device/internal/errors" + "v.io/x/ref/services/internal/binarylib" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/test" + "v.io/x/ref/test/expect" + "v.io/x/ref/test/testutil" +) + +func TestMain(m *testing.M) { + utiltest.TestMainImpl(m) +} + +// TestSuidHelper is testing boilerplate for suidhelper that does not +// create a runtime because the suidhelper is not a Vanadium application. +func TestSuidHelper(t *testing.T) { + utiltest.TestSuidHelperImpl(t) +} + +// TODO(rjkroege): generateDeviceManagerScript and generateSuidHelperScript have +// code similarity that might benefit from refactoring. +// generateDeviceManagerScript is very similar in behavior to generateScript in +// device_invoker.go. However, we chose to re-implement it here for two +// reasons: (1) avoid making generateScript public; and (2) how the test choses +// to invoke the device manager subprocess the first time should be independent +// of how device manager implementation sets up its updated versions. +func generateDeviceManagerScript(t *testing.T, root string, args, env []string) string { + env = impl.VanadiumEnvironment(env) + output := "#!" + impl.ShellPath + "\n" + output += strings.Join(config.QuoteEnv(env), " ") + " exec " + output += strings.Join(args, " ") + if err := os.MkdirAll(filepath.Join(root, "factory"), 0755); err != nil { + t.Fatalf("MkdirAll failed: %v", err) + } + // Why pigeons? To show that the name we choose for the initial script + // doesn't matter and in particular is independent of how device manager + // names its updated version scripts (deviced.sh). + path := filepath.Join(root, "factory", "pigeons.sh") + if err := ioutil.WriteFile(path, []byte(output), 0755); err != nil { + t.Fatalf("WriteFile(%v) failed: %v", path, err) + } + return path +} + +// TestDeviceManagerUpdateAndRevert makes the device manager go through the +// motions of updating itself to newer versions (twice), and reverting itself +// back (twice). It also checks that update and revert fail when they're +// supposed to. The initial device manager is running 'by hand' via a module +// command. Further versions are running through the soft link that the device +// manager itself updates. +func TestDeviceManagerUpdateAndRevert(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + sh, deferFn := servicetest.CreateShellAndMountTable(t, ctx) + defer deferFn() + + // Set up mock application and binary repositories. + envelope, cleanup := utiltest.StartMockRepos(t, ctx) + defer cleanup() + + root, cleanup := servicetest.SetupRootDir(t, "devicemanager") + defer cleanup() + if err := versioning.SaveCreatorInfo(ctx, root); err != nil { + t.Fatal(err) + } + + // Current link does not have to live in the root dir, but it's + // convenient to put it there so we have everything in one place. + currLink := filepath.Join(root, "current_link") + + // Since the device manager will be restarting, use the + // VeyronCredentials environment variable to maintain the same set of + // credentials across runs. + // Without this, authentication/authorization state - such as the blessings of + // the device manager and the signatures used for AccessList integrity checks + // - will not carry over between updates to the binary, which would not be + // reflective of intended use. + dmVars := map[string]string{ref.EnvCredentials: utiltest.CreatePrincipal(t, sh)} + dmArgs := []interface{}{"factoryDM", root, "unused_helper", utiltest.MockApplicationRepoName, currLink} + dmCmd := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, dmArgs...) + dmCmd.Vars = envvar.MergeMaps(dmCmd.Vars, dmVars) + scriptPathFactory := generateDeviceManagerScript(t, root, dmCmd.Args, envvar.MapToSlice(dmCmd.Vars)) + + if err := os.Symlink(scriptPathFactory, currLink); err != nil { + t.Fatalf("Symlink(%q, %q) failed: %v", scriptPathFactory, currLink, err) + } + + // We instruct the initial device manager that we run to pause before + // stopping its service, so that we get a chance to verify that + // attempting an update while another one is ongoing will fail. + dmPauseBeforeStopVars := envvar.MergeMaps(dmVars, map[string]string{"PAUSE_BEFORE_STOP": "1"}) + + // Start the initial version of the device manager, the so-called + // "factory" version. We use the v23test-generated command to start it. + // We could have also used the scriptPathFactory to start it, but this + // demonstrates that the initial device manager could be running by hand + // as long as the right initial configuration is passed into the device + // manager implementation. + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, dmArgs...) + dm.Vars = envvar.MergeMaps(dm.Vars, dmPauseBeforeStopVars) + dmStdin := dm.StdinPipe() + dm.Start() + dm.S.Expect("READY") + defer func() { + dm.Terminate(os.Interrupt) + utiltest.VerifyNoRunningProcesses(t) + }() + + utiltest.Resolve(t, ctx, "claimable", 1, true) + // Brand new device manager must be claimed first. + utiltest.ClaimDevice(t, ctx, "claimable", "factoryDM", "mydevice", utiltest.NoPairingToken) + + if v := utiltest.VerifyDeviceState(t, ctx, device.InstanceStateRunning, "factoryDM"); v != "factory" { + t.Errorf("Expected factory version, got %v instead", v) + } + + // Simulate an invalid envelope in the application repository. + *envelope = utiltest.EnvelopeFromShell(sh, envvar.MapToSlice(dmVars), nil, utiltest.DeviceManager, "bogus", 0, 0, dmArgs...) + + utiltest.UpdateDeviceExpectError(t, ctx, "factoryDM", errors.ErrAppTitleMismatch.ID) + utiltest.RevertDeviceExpectError(t, ctx, "factoryDM", errors.ErrUpdateNoOp.ID) + + // Set up a second version of the device manager. The information in the + // envelope will be used by the device manager to stage the next + // version. + *envelope = utiltest.EnvelopeFromShell(sh, envvar.MapToSlice(dmVars), nil, utiltest.DeviceManager, application.DeviceManagerTitle, 0, 0, "v2DM") + utiltest.UpdateDevice(t, ctx, "factoryDM") + + // Current link should have been updated to point to v2. + evalLink := func() string { + path, err := filepath.EvalSymlinks(currLink) + if err != nil { + t.Fatalf("EvalSymlinks(%v) failed: %v", currLink, err) + } + return path + } + scriptPathV2 := evalLink() + if scriptPathFactory == scriptPathV2 { + t.Fatalf("current link didn't change") + } + v2 := utiltest.VerifyDeviceState(t, ctx, device.InstanceStateUpdating, "factoryDM") + + utiltest.UpdateDeviceExpectError(t, ctx, "factoryDM", errors.ErrOperationInProgress.ID) + + dmStdin.Close() + dm.S.Expect("restart handler") + dm.S.Expect("factoryDM terminated") + + // A successful update means the device manager has stopped itself. We + // relaunch it from the current link. + utiltest.ResolveExpectNotFound(t, ctx, "v2DM", false) // Ensure a clean slate. + + dm = sh.FuncCmd(utiltest.ExecScript, currLink) + dm.Vars = envvar.MergeMaps(dm.Vars, dmVars) + dm.Start() + dm.S.Expect("READY") + + utiltest.Resolve(t, ctx, "v2DM", 1, true) // Current link should have been launching v2. + + // Try issuing an update without changing the envelope in the + // application repository: this should fail, and current link should be + // unchanged. + utiltest.UpdateDeviceExpectError(t, ctx, "v2DM", errors.ErrUpdateNoOp.ID) + if evalLink() != scriptPathV2 { + t.Fatalf("script changed") + } + + // Try issuing an update with a binary that has a different major version + // number. It should fail. + utiltest.ResolveExpectNotFound(t, ctx, "v2.5DM", false) // Ensure a clean slate. + *envelope = utiltest.EnvelopeFromShell(sh, envvar.MapToSlice(dmVars), nil, utiltest.DeviceManagerV10, application.DeviceManagerTitle, 0, 0, "v2.5DM") + utiltest.UpdateDeviceExpectError(t, ctx, "v2DM", errors.ErrOperationFailed.ID) + + if evalLink() != scriptPathV2 { + t.Fatalf("script changed") + } + + // Create a third version of the device manager and issue an update. + *envelope = utiltest.EnvelopeFromShell(sh, envvar.MapToSlice(dmVars), nil, utiltest.DeviceManager, application.DeviceManagerTitle, 0, 0, "v3DM") + utiltest.UpdateDevice(t, ctx, "v2DM") + + scriptPathV3 := evalLink() + if scriptPathV3 == scriptPathV2 { + t.Fatalf("current link didn't change") + } + + dm.S.Expect("restart handler") + dm.S.Expect("v2DM terminated") + + utiltest.ResolveExpectNotFound(t, ctx, "v3DM", false) // Ensure a clean slate. + + // Re-launch the device manager from current link. We instruct the + // device manager to pause before stopping its server, so that we can + // verify that a second revert fails while a revert is in progress. + dm = sh.FuncCmd(utiltest.ExecScript, currLink) + dm.Vars = envvar.MergeMaps(dm.Vars, dmPauseBeforeStopVars) + dmStdin = dm.StdinPipe() + dm.Start() + dm.S.Expect("READY") + + utiltest.Resolve(t, ctx, "v3DM", 1, true) // Current link should have been launching v3. + v3 := utiltest.VerifyDeviceState(t, ctx, device.InstanceStateRunning, "v3DM") + if v2 == v3 { + t.Fatalf("version didn't change") + } + + // Revert the device manager to its previous version (v2). + utiltest.RevertDevice(t, ctx, "v3DM") + utiltest.RevertDeviceExpectError(t, ctx, "v3DM", errors.ErrOperationInProgress.ID) // Revert already in progress. + dmStdin.Close() + dm.S.Expect("restart handler") + dm.S.Expect("v3DM terminated") + if evalLink() != scriptPathV2 { + t.Fatalf("current link was not reverted correctly") + } + + utiltest.ResolveExpectNotFound(t, ctx, "v2DM", false) // Ensure a clean slate. + + dm = sh.FuncCmd(utiltest.ExecScript, currLink) + dm.Vars = envvar.MergeMaps(dm.Vars, dmVars) + dm.Start() + dm.S.Expect("READY") + + utiltest.Resolve(t, ctx, "v2DM", 1, true) // Current link should have been launching v2. + + // Revert the device manager to its previous version (factory). + utiltest.RevertDevice(t, ctx, "v2DM") + dm.S.Expect("restart handler") + dm.S.Expect("v2DM terminated") + dm.S.ExpectEOF() + if evalLink() != scriptPathFactory { + t.Fatalf("current link was not reverted correctly") + } + + utiltest.ResolveExpectNotFound(t, ctx, "factoryDM", false) // Ensure a clean slate. + + dm = sh.FuncCmd(utiltest.ExecScript, currLink) + dm.Vars = envvar.MergeMaps(dm.Vars, dmVars) + dm.Start() + dm.S.Expect("READY") + + utiltest.Resolve(t, ctx, "factoryDM", 1, true) // Current link should have been launching factory version. + utiltest.ShutdownDevice(t, ctx, "factoryDM") + dm.S.Expect("factoryDM terminated") + dm.S.ExpectEOF() + + // Re-launch the device manager, to exercise the behavior of Stop. + utiltest.ResolveExpectNotFound(t, ctx, "factoryDM", false) // Ensure a clean slate. + dm = sh.FuncCmd(utiltest.ExecScript, currLink) + dm.Vars = envvar.MergeMaps(dm.Vars, dmVars) + dm.Start() + dm.S.Expect("READY") + + utiltest.Resolve(t, ctx, "factoryDM", 1, true) + utiltest.KillDevice(t, ctx, "factoryDM") + dm.S.Expect("restart handler") + dm.S.Expect("factoryDM terminated") + dm.S.ExpectEOF() +} + +type simpleRW chan []byte + +func (s simpleRW) Write(p []byte) (n int, err error) { + s <- p + return len(p), nil +} +func (s simpleRW) Read(p []byte) (n int, err error) { + return copy(p, <-s), nil +} + +// TestDeviceManagerInstallation verifies the 'self install' and 'uninstall' +// functionality of the device manager: it runs SelfInstall in a child process, +// then runs the executable from the soft link that the installation created. +// This should bring up a functioning device manager. In the end it runs +// Uninstall and verifies that the installation is gone. +func TestDeviceManagerInstallation(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + sh, deferFn := servicetest.CreateShellAndMountTable(t, ctx) + defer deferFn() + testDir, cleanup := servicetest.SetupRootDir(t, "devicemanager") + defer cleanup() + // No need to call SaveCreatorInfo() here because that's part of SelfInstall below + + // Create a script wrapping the test target that implements suidhelper. + suidHelperPath := utiltest.GenerateSuidHelperScript(t, testDir) + // Create a dummy script mascarading as the restarter. + restarterPath := utiltest.GenerateRestarter(t, testDir) + initHelperPath := "" + + // Create an 'envelope' for the device manager that we can pass to the + // installer, to ensure that the device manager that the installer + // configures can run. + dmCmd := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm") + dmDir := filepath.Join(testDir, "dm") + // TODO(caprita): Add test logic when initMode = true. + singleUser, sessionMode, initMode := true, true, false + if err := installer.SelfInstall(ctx, dmDir, suidHelperPath, restarterPath, "", initHelperPath, "", singleUser, sessionMode, initMode, dmCmd.Args[1:], envvar.MapToSlice(dmCmd.Vars), os.Stderr, os.Stdout); err != nil { + t.Fatalf("SelfInstall failed: %v", err) + } + + utiltest.ResolveExpectNotFound(t, ctx, "dm", false) + // Start the device manager. + stdout := make(simpleRW, 100) + defer os.Setenv(utiltest.RedirectEnv, os.Getenv(utiltest.RedirectEnv)) + os.Setenv(utiltest.RedirectEnv, "1") + if err := installer.Start(ctx, dmDir, os.Stderr, stdout); err != nil { + t.Fatalf("Start failed: %v", err) + } + dms := expect.NewSession(t, stdout, servicetest.ExpectTimeout) + dms.Expect("READY") + utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken) + utiltest.RevertDeviceExpectError(t, ctx, "dm", errors.ErrUpdateNoOp.ID) // No previous version available. + + // Stop the device manager. + if err := installer.Stop(ctx, dmDir, os.Stderr, os.Stdout); err != nil { + t.Fatalf("Stop failed: %v", err) + } + dms.Expect("dm terminated") + + // Uninstall. + if err := installer.Uninstall(ctx, dmDir, suidHelperPath, os.Stderr, os.Stdout); err != nil { + t.Fatalf("Uninstall failed: %v", err) + } + // Ensure that the installation is gone. + if files, err := ioutil.ReadDir(dmDir); err != nil || len(files) > 0 { + var finfo []string + for _, f := range files { + finfo = append(finfo, f.Name()) + } + t.Fatalf("ReadDir returned (%v, %v)", err, finfo) + } +} + +// TODO(caprita): We need better test coverage for how updating/reverting apps +// affects the package configured for the app. +func TestDeviceManagerPackages(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + sh, deferFn := servicetest.CreateShellAndMountTable(t, ctx) + defer deferFn() + + // Set up mock application and binary repositories. + envelope, cleanup := utiltest.StartMockRepos(t, ctx) + defer cleanup() + + binaryVON := "realbin" + defer utiltest.StartRealBinaryRepository(t, ctx, binaryVON)() + + // upload package to binary repository + tmpdir, err := ioutil.TempDir("", "test-package-") + if err != nil { + t.Fatalf("ioutil.TempDir failed: %v", err) + } + defer os.RemoveAll(tmpdir) + createFile := func(name, contents string) { + if err := ioutil.WriteFile(filepath.Join(tmpdir, name), []byte(contents), 0600); err != nil { + t.Fatalf("ioutil.WriteFile failed: %v", err) + } + } + createFile("hello.txt", "Hello World!") + if _, err := binarylib.UploadFromDir(ctx, naming.Join(binaryVON, "testpkg"), tmpdir); err != nil { + t.Fatalf("binarylib.UploadFromDir failed: %v", err) + } + createAndUpload := func(von, contents string) { + createFile("tempfile", contents) + if _, err := binarylib.UploadFromFile(ctx, naming.Join(binaryVON, von), filepath.Join(tmpdir, "tempfile")); err != nil { + t.Fatalf("binarylib.UploadFromFile failed: %v", err) + } + } + createAndUpload("testfile", "Goodbye World!") + createAndUpload("leftshark", "Left shark") + createAndUpload("rightshark", "Right shark") + createAndUpload("beachball", "Beach ball") + + root, cleanup := servicetest.SetupRootDir(t, "devicemanager") + defer cleanup() + if err := versioning.SaveCreatorInfo(ctx, root); err != nil { + t.Fatal(err) + } + + // Create a script wrapping the test target that implements suidhelper. + helperPath := utiltest.GenerateSuidHelperScript(t, root) + + // Set up the device manager. Since we won't do device manager updates, + // don't worry about its application envelope and current link. + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link") + dm.Start() + dm.S.Expect("READY") + defer func() { + dm.Terminate(os.Interrupt) + dm.S.Expect("dm terminated") + utiltest.VerifyNoRunningProcesses(t) + }() + + // Create the local server that the app uses to let us know it's ready. + pingCh, cleanup := utiltest.SetupPingServer(t, ctx) + defer cleanup() + + // Create the envelope for the first version of the app. + *envelope = utiltest.EnvelopeFromShell(sh, nil, nil, utiltest.App, "google naps", 0, 0, "appV1") + envelope.Packages = map[string]application.SignedFile{ + "test": application.SignedFile{ + File: "realbin/testpkg", + }, + "test2": application.SignedFile{ + File: "realbin/testfile", + }, + "shark": application.SignedFile{ + File: "realbin/leftshark", + }, + } + + // These are install-time overrides for packages. + // Specifically, we override the 'shark' package and add a new + // 'ball' package on top of what's specified in the envelope. + packages := application.Packages{ + "shark": application.SignedFile{ + File: "realbin/rightshark", + }, + "ball": application.SignedFile{ + File: "realbin/beachball", + }, + } + // Device must be claimed before apps can be installed. + utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken) + // Install the app. + appID := utiltest.InstallApp(t, ctx, packages) + + // Start an instance of the app. + instance1ID := utiltest.LaunchApp(t, ctx, appID) + defer utiltest.TerminateApp(t, ctx, appID, instance1ID) + + // Wait until the app pings us that it's ready. + pingCh.WaitForPingArgs(t) + + for _, c := range []struct { + path, content string + }{ + { + filepath.Join("test", "hello.txt"), + "Hello World!", + }, + { + "test2", + "Goodbye World!", + }, + { + "shark", + "Right shark", + }, + { + "ball", + "Beach ball", + }, + } { + // Ask the app to cat the file. + file := filepath.Join("packages", c.path) + name := "appV1" + content, err := utiltest.Cat(ctx, name, file) + if err != nil { + t.Errorf("utiltest.Cat(%q, %q) failed: %v", name, file, err) + } + if expected := c.content; content != expected { + t.Errorf("unexpected content: expected %q, got %q", expected, content) + } + } +} + +func listAndVerifyAssociations(t *testing.T, ctx *context.T, stub device.DeviceClientMethods, expected []device.Association) { + assocs, err := stub.ListAssociations(ctx) + if err != nil { + t.Fatalf("ListAssociations failed %v", err) + } + utiltest.CompareAssociations(t, assocs, expected) +} + +// TODO(rjkroege): Verify that associations persist across restarts once +// permanent storage is added. +func TestAccountAssociation(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + // Use a common root identity provider so that all principals can talk to one + // another. + idp := testutil.NewIDProvider("root") + if err := idp.Bless(v23.GetPrincipal(ctx), "ctx"); err != nil { + t.Fatal(err) + } + + sh, deferFn := servicetest.CreateShellAndMountTable(t, ctx) + defer deferFn() + + root, cleanup := servicetest.SetupRootDir(t, "devicemanager") + defer cleanup() + if err := versioning.SaveCreatorInfo(ctx, root); err != nil { + t.Fatal(err) + } + + // By default, the two processes (selfCtx and otherCtx) will have blessings + // generated based on the username/machine name running this process. Since + // these blessings will appear in AccessLists, give them recognizable names. + selfCtx := utiltest.CtxWithNewPrincipal(t, ctx, idp, "self") + otherCtx := utiltest.CtxWithNewPrincipal(t, selfCtx, idp, "other") + + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, "unused_helper", "unused_app_repo_name", "unused_curr_link") + dm.Start() + dm.S.Expect("READY") + defer func() { + dm.Terminate(os.Interrupt) + dm.S.Expect("dm terminated") + utiltest.VerifyNoRunningProcesses(t) + }() + + // Attempt to list associations on the device manager without having + // claimed it. + if list, err := device.DeviceClient("claimable").ListAssociations(otherCtx); err == nil { + t.Fatalf("ListAssociations should fail on unclaimed device manager but did not: (%v, %v)", list, err) + } + + // self claims the device manager. + utiltest.ClaimDevice(t, selfCtx, "claimable", "dm", "alice", utiltest.NoPairingToken) + + ctx.VI(2).Info("Verify that associations start out empty.") + deviceStub := device.DeviceClient("dm/device") + listAndVerifyAssociations(t, selfCtx, deviceStub, []device.Association(nil)) + + if err := deviceStub.AssociateAccount(selfCtx, []string{"root/self", "root/other"}, "alice_system_account"); err != nil { + t.Fatalf("ListAssociations failed %v", err) + } + ctx.VI(2).Info("Added association should appear.") + listAndVerifyAssociations(t, selfCtx, deviceStub, []device.Association{ + { + "root/self", + "alice_system_account", + }, + { + "root/other", + "alice_system_account", + }, + }) + + if err := deviceStub.AssociateAccount(selfCtx, []string{"root/self", "root/other"}, "alice_other_account"); err != nil { + t.Fatalf("AssociateAccount failed %v", err) + } + ctx.VI(2).Info("Change the associations and the change should appear.") + listAndVerifyAssociations(t, selfCtx, deviceStub, []device.Association{ + { + "root/self", + "alice_other_account", + }, + { + "root/other", + "alice_other_account", + }, + }) + + if err := deviceStub.AssociateAccount(selfCtx, []string{"root/other"}, ""); err != nil { + t.Fatalf("AssociateAccount failed %v", err) + } + ctx.VI(2).Info("Verify that we can remove an association.") + listAndVerifyAssociations(t, selfCtx, deviceStub, []device.Association{ + { + "root/self", + "alice_other_account", + }, + }) +} diff --git a/x/ref/services/device/deviced/internal/impl/instance_reaping.go b/x/ref/services/device/deviced/internal/impl/instance_reaping.go new file mode 100644 index 000000000..4b409a803 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/instance_reaping.go @@ -0,0 +1,292 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "fmt" + "path/filepath" + "sync" + "syscall" + "time" + + "v.io/v23/context" + "v.io/v23/services/device" + "v.io/v23/verror" +) + +var errPIDIsNotInteger = verror.Register(pkgPath+".errPIDIsNotInteger", verror.NoRetry, "{1:}{2:} __debug/stats/system/pid isn't an integer{:_}") + +type pidInstanceDirPair struct { + instanceDir string + pid int +} + +type reaper struct { + c chan pidInstanceDirPair + stopped chan struct{} + ctx *context.T +} + +var stashedPidMap map[string]int + +func newReaper(ctx *context.T, root string, appRunner *appRunner) (*reaper, error) { + pidMap, err := findAllTheInstances(ctx, root) + + // Used only by the testing code that verifies that all processes + // have been shutdown. + stashedPidMap = pidMap + if err != nil { + return nil, err + } + + r := &reaper{ + c: make(chan pidInstanceDirPair), + stopped: make(chan struct{}), + ctx: ctx, + } + // Restart daemon jobs if they're not running (say because the machine crashed.) + go r.processStatusPolling(ctx, pidMap, appRunner) + return r, nil +} + +func markNotRunning(ctx *context.T, runner *appRunner, idir string) error { + if err := runner.principalMgr.StopServing(idir); err != nil { + return fmt.Errorf("StopServing(%v) failed: %v", idir, err) + } + + if instanceStateIs(idir, device.InstanceStateNotRunning) { + return nil + } + // If the app is not in state Running, it is likely in the process of + // being launched or killed when the reaper poll finds the process dead. + // Do not attempt a restart in this case. + return transitionInstance(idir, device.InstanceStateRunning, device.InstanceStateNotRunning) +} + +func isAlive(ctx *context.T, pid int) bool { + switch err := syscall.Kill(pid, 0); err { + case syscall.ESRCH: + // No such PID. + return false + case nil, syscall.EPERM: + return true + default: + // The kill system call manpage says that this can only happen + // if the kernel claims that 0 is an invalid signal. Only a + // deeply confused kernel would say this so just give up. + ctx.Panicf("processStatusPolling: unanticipated result from sys.Kill: %v", err) + return true + } +} + +// processStatusPolling polls for the continued existence of a set of +// tracked pids. TODO(rjkroege): There are nicer ways to provide this +// functionality. For example, use the kevent facility in darwin or +// replace init. See http://www.incenp.org/dvlpt/wait4.html for +// inspiration. +func (r *reaper) processStatusPolling(ctx *context.T, trackedPids map[string]int, appRunner *appRunner) { + poll := func(ctx *context.T) { + for idir, pid := range trackedPids { + if !isAlive(ctx, pid) { + ctx.Infof("processStatusPolling discovered %v (pid %d) ended", idir, pid) + if err := markNotRunning(ctx, appRunner, idir); err != nil { + ctx.Errorf("markNotRunning failed: %v", err) + } else { + go appRunner.restartAppIfNecessary(ctx, idir) + } + delete(trackedPids, idir) + } else { + ctx.VI(2).Infof("processStatusPolling saw live pid: %d", pid) + // The task exists and is running under the same uid as + // the device manager or the task exists and is running + // under a different uid as would be the case if invoked + // via suidhelper. In this case do, nothing. + + // This implementation cannot detect if a process exited + // and was replaced by an arbitrary non-Vanadium process + // within the polling interval. + // TODO(rjkroege): Probe the appcycle service of the app + // to confirm that its pid is valid. + + // TODO(rjkroege): if we can't connect to the app here via + // the appcycle manager, the app was probably started under + // a different agent and cannot be managed. Perhaps we should + // then kill the app and restart it? + } + } + } + + for { + select { + case p := <-r.c: + switch { + case p.instanceDir == "": + return // Shutdown. + case p.pid == -1: // stop watching this instance + delete(trackedPids, p.instanceDir) + poll(ctx) + case p.pid == -2: // kill the process + info, err := loadInstanceInfo(ctx, p.instanceDir) + if err != nil { + ctx.Errorf("loadInstanceInfo(%v) failed: %v", p.instanceDir, err) + continue + } + if info.Pid <= 0 { + ctx.Errorf("invalid pid in %v: %v", p.instanceDir, info.Pid) + continue + } + if err := suidHelper.terminatePid(ctx, info.Pid, nil, nil); err != nil { + ctx.Errorf("Failure to kill pid %d: %v", info.Pid, err) + } + case p.pid < 0: + ctx.Panicf("invalid pid %v", p.pid) + default: + trackedPids[p.instanceDir] = p.pid + poll(ctx) + } + case <-time.After(time.Second): + // Poll once / second. + // TODO(caprita): Configure this to use timekeeper to + // allow simulated time injection for testing. + poll(ctx) + } + } +} + +func (r *reaper) sendCmd(idir string, pid int) { + select { + case r.c <- pidInstanceDirPair{instanceDir: idir, pid: pid}: + case <-r.stopped: + } +} + +// startWatching begins watching process pid's state. This routine +// assumes that pid already exists. Since pid is delivered to the device +// manager by RPC callback, this seems reasonable. +func (r *reaper) startWatching(idir string, pid int) { + r.sendCmd(idir, pid) +} + +// stopWatching stops watching process pid's state. +func (r *reaper) stopWatching(idir string) { + r.sendCmd(idir, -1) +} + +// forciblySuspend terminates the process pid. +func (r *reaper) forciblySuspend(idir string) { + r.sendCmd(idir, -2) +} + +// shutdown stops the reaper. +func (r *reaper) shutdown() { + r.sendCmd("", 0) + close(r.stopped) +} + +type pidErrorTuple struct { + ipath string + pid int + err error +} + +// processStatusViaKill updates the status based on sending a kill signal +// to the process. This assumes that most processes on the system are +// likely to be managed by the device manager and a live process is not +// responsive because the agent has been restarted rather than being +// created through a different means. +func processStatusViaKill(ctx *context.T, c chan<- pidErrorTuple, instancePath string, info *instanceInfo, state device.InstanceState) { + pid := info.Pid + + switch err := syscall.Kill(pid, 0); err { + case syscall.ESRCH: + // No such PID. + if err := transitionInstance(instancePath, state, device.InstanceStateNotRunning); err != nil { + ctx.Errorf("transitionInstance(%s,%s,%s) failed: %v", instancePath, state, device.InstanceStateNotRunning, err) + } + // We only want to restart apps that were Running or Launching. + if state == device.InstanceStateLaunching || state == device.InstanceStateRunning { + c <- pidErrorTuple{ipath: instancePath, pid: pid, err: err} + } + case nil, syscall.EPERM: + // The instance was found to be running, so update its state. + if err := transitionInstance(instancePath, state, device.InstanceStateRunning); err != nil { + ctx.Errorf("transitionInstance(%s,%v, %v) failed: %v", instancePath, state, device.InstanceStateRunning, err) + } + ctx.VI(0).Infof("perInstance go routine for %v ending", instancePath) + c <- pidErrorTuple{ipath: instancePath, pid: pid} + } +} + +func perInstance(ctx *context.T, instancePath string, c chan<- pidErrorTuple, wg *sync.WaitGroup) { + defer wg.Done() + ctx.Infof("Instance: %v", instancePath) + state, err := getInstanceState(instancePath) + switch state { + // Ignore apps already in deleted and not running states. + case device.InstanceStateNotRunning: + return + case device.InstanceStateDeleted: + return + // If the app was updating, it means it was already not running, so just + // update its state back to not running. + case device.InstanceStateUpdating: + if err := transitionInstance(instancePath, state, device.InstanceStateNotRunning); err != nil { + ctx.Errorf("transitionInstance(%s,%s,%s) failed: %v", instancePath, state, device.InstanceStateNotRunning, err) + } + return + } + ctx.VI(2).Infof("perInstance firing up on %s", instancePath) + + // Read the instance data. + info, err := loadInstanceInfo(ctx, instancePath) + if err != nil { + ctx.Errorf("loadInstanceInfo failed: %v", err) + // Something has gone badly wrong. + // TODO(rjkroege,caprita): Consider removing the instance or at + // least set its state to something indicating error? + c <- pidErrorTuple{err: err, ipath: instancePath} + return + } + + // Remaining states: Launching, Running, Dying. Of these, + // daemon mode will restart Launching and Running if the process + // is not alive. + processStatusViaKill(ctx, c, instancePath, info, state) +} + +// Digs through the directory hierarchy. +func findAllTheInstances(ctx *context.T, root string) (map[string]int, error) { + paths, err := filepath.Glob(filepath.Join(root, "app*", "installation*", "instances", "instance*")) + if err != nil { + return nil, err + } + + pidmap := make(map[string]int) + pidchan := make(chan pidErrorTuple, len(paths)) + var wg sync.WaitGroup + + for _, pth := range paths { + wg.Add(1) + go perInstance(ctx, pth, pidchan, &wg) + } + wg.Wait() + close(pidchan) + + for p := range pidchan { + if p.err != nil { + ctx.Errorf("instance at %s had an error: %v", p.ipath, p.err) + } + if p.pid > 0 { + pidmap[p.ipath] = p.pid + } + } + return pidmap, nil +} + +// RunningChildrenProcesses uses the reaper to verify that a test has +// successfully shut down all processes. +func RunningChildrenProcesses() bool { + return len(stashedPidMap) > 0 +} diff --git a/x/ref/services/device/deviced/internal/impl/only_for_test.go b/x/ref/services/device/deviced/internal/impl/only_for_test.go new file mode 100644 index 000000000..9b02bb79d --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/only_for_test.go @@ -0,0 +1,31 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "fmt" + + "v.io/v23/rpc" +) + +// This file contains code in the impl package that we only want built for tests +// (it exposes public API methods that we don't want to normally expose). + +func (c *callbackState) leaking() bool { + c.Lock() + defer c.Unlock() + return len(c.channels) > 0 +} + +func DispatcherLeaking(d rpc.Dispatcher) bool { + switch obj := d.(type) { + case *dispatcher: + return obj.internal.callback.leaking() + case *testModeDispatcher: + return obj.realDispatcher.(*dispatcher).internal.callback.leaking() + default: + panic(fmt.Sprintf("unexpected type: %T", d)) + } +} diff --git a/x/ref/services/device/deviced/internal/impl/perms/debug_perms_test.go b/x/ref/services/device/deviced/internal/impl/perms/debug_perms_test.go new file mode 100644 index 000000000..bb9571803 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/perms/debug_perms_test.go @@ -0,0 +1,264 @@ +// Copyright 2015 The Vanadium 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 perms_test + +import ( + "io/ioutil" + "os" + "testing" + + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/permissions" + "v.io/v23/verror" + + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" + "v.io/x/ref/test/testutil" +) + +func updateAccessList(t *testing.T, ctx *context.T, blessing, right string, name ...string) { + accessStub := permissions.ObjectClient(naming.Join(name...)) + perms, version, err := accessStub.GetPermissions(ctx) + if err != nil { + t.Fatal(testutil.FormatLogLine(2, "GetPermissions(%v) failed %v", name, err)) + } + perms.Add(security.BlessingPattern(blessing), right) + if err = accessStub.SetPermissions(ctx, perms, version); err != nil { + t.Fatal(testutil.FormatLogLine(2, "SetPermissions(%v, %v, %v) failed: %v", name, blessing, right, err)) + } +} + +func testAccessFail(t *testing.T, expected verror.ID, ctx *context.T, who string, name ...string) { + if _, err := utiltest.StatsStub(name...).Value(ctx); verror.ErrorID(err) != expected { + t.Fatal(testutil.FormatLogLine(2, "%s got error %v but expected %v", who, err, expected)) + } +} + +func TestDebugPermissionsPropagation(t *testing.T) { + cleanup, ctx, sh, envelope, root, helperPath, idp := utiltest.StartupHelper(t) + defer cleanup() + + // Set up the device manager. + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link") + dm.Start() + dm.S.Expect("READY") + utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken) + + // Create the local server that the app uses to let us know it's ready. + pingCh, cleanup := utiltest.SetupPingServer(t, ctx) + defer cleanup() + utiltest.Resolve(t, ctx, "pingserver", 1, true) + + // Make some users. + selfCtx := ctx + bobCtx := utiltest.CtxWithNewPrincipal(t, selfCtx, idp, "bob") + hjCtx := utiltest.CtxWithNewPrincipal(t, selfCtx, idp, "hackerjoe") + aliceCtx := utiltest.CtxWithNewPrincipal(t, selfCtx, idp, "alice") + + // TODO(rjkroege): Set AccessLists here that conflict with the one provided by the device + // manager and show that the one set here is overridden. + // Create the envelope for the first version of the app. + *envelope = utiltest.EnvelopeFromShell(sh, nil, nil, utiltest.App, "google naps", 0, 0, "appV1") + + // Install the app. + appID := utiltest.InstallApp(t, ctx) + + // Give bob rights to start an app. + updateAccessList(t, selfCtx, "root:bob:$", string(access.Read), "dm/apps", appID) + + // Bob starts an instance of the app. + bobApp := utiltest.LaunchApp(t, bobCtx, appID) + pingCh.VerifyPingArgs(t, utiltest.UserName(t), "default", "") + + // Bob permits Alice to read from his app. + updateAccessList(t, bobCtx, "root:alice:$", string(access.Read), "dm/apps", appID, bobApp) + + // Create some globbing test vectors. + globtests := []utiltest.GlobTestVector{ + {naming.Join("dm", "apps", appID, bobApp), "*", + []string{"logs", "pprof", "stats"}, + }, + {naming.Join("dm", "apps", appID, bobApp, "stats", "system"), + "start-time*", + []string{"start-time-rfc1123", "start-time-unix"}, + }, + {naming.Join("dm", "apps", appID, bobApp, "logs"), + "*", + []string{ + "STDERR-<timestamp>", + "STDOUT-<timestamp>", + "app.INFO", + "app.<*>.INFO.<timestamp>", + }, + }, + } + appGlobtests := []utiltest.GlobTestVector{ + {naming.Join("appV1", "__debug"), "*", + []string{"http", "logs", "pprof", "stats", "vtrace"}, + }, + {naming.Join("appV1", "__debug", "stats", "system"), + "start-time*", + []string{"start-time-rfc1123", "start-time-unix"}, + }, + {naming.Join("appV1", "__debug", "logs"), + "*", + []string{ + "STDERR-<timestamp>", + "STDOUT-<timestamp>", + "app.INFO", + "app.<*>.INFO.<timestamp>", + }, + }, + } + globtestminus := globtests[1:] + res := utiltest.NewGlobTestRegexHelper("app") + + // Confirm that self can access __debug names. + utiltest.VerifyGlob(t, selfCtx, "app", globtests, res) + utiltest.VerifyStatsValues(t, selfCtx, "dm", "apps", appID, bobApp, "stats/system/start-time*") + utiltest.VerifyLog(t, selfCtx, "dm", "apps", appID, bobApp, "logs", "*") + utiltest.VerifyPProfCmdLine(t, selfCtx, "app", "dm", "apps", appID, bobApp, "pprof") + + // Bob started the app so selfCtx can't connect to the app. + utiltest.VerifyFailGlob(t, selfCtx, appGlobtests) + testAccessFail(t, verror.ErrNoAccess.ID, selfCtx, "self", "appV1", "__debug", "stats/system/pid") + + // hackerjoe (for example) can't either. + utiltest.VerifyFailGlob(t, hjCtx, appGlobtests) + testAccessFail(t, verror.ErrNoAccess.ID, hjCtx, "hackerjoe", "appV1", "__debug", "stats/system/pid") + + // Bob has an issue with his app and tries to use the debug output to figure it out. + utiltest.VerifyGlob(t, bobCtx, "app", globtests, res) + utiltest.VerifyStatsValues(t, bobCtx, "dm", "apps", appID, bobApp, "stats/system/start-time*") + utiltest.VerifyLog(t, bobCtx, "dm", "apps", appID, bobApp, "logs", "*") + utiltest.VerifyPProfCmdLine(t, bobCtx, "app", "dm", "apps", appID, bobApp, "pprof") + + // Bob can also connect directly to his app. + utiltest.VerifyGlob(t, bobCtx, "app", appGlobtests, res) + utiltest.VerifyStatsValues(t, bobCtx, "appV1", "__debug", "stats/system/start-time*") + + // But Bob can't figure it out and hopes that hackerjoe can debug it. + updateAccessList(t, bobCtx, "root:hackerjoe:$", string(access.Debug), "dm/apps", appID, bobApp) + + // Fortunately the device manager permits hackerjoe to access the stats. + // But hackerjoe can't solve Bob's problem. + // Because hackerjoe has Debug, hackerjoe can glob the __debug resources + // of Bob's app but can't glob Bob's app. + utiltest.VerifyGlob(t, hjCtx, "app", globtestminus, res) + utiltest.VerifyFailGlob(t, hjCtx, globtests[0:1]) + utiltest.VerifyStatsValues(t, hjCtx, "dm", "apps", appID, bobApp, "stats", "system/start-time*") + utiltest.VerifyLog(t, hjCtx, "dm", "apps", appID, bobApp, "logs", "*") + utiltest.VerifyPProfCmdLine(t, hjCtx, "app", "dm", "apps", appID, bobApp, "pprof") + + // Permissions are propagated to the app so hackerjoe can connect + // directly to the app too. + utiltest.VerifyGlob(t, hjCtx, "app", globtestminus, res) + utiltest.VerifyStatsValues(t, hjCtx, "appV1", "__debug", "stats/system/start-time*") + + // Alice might be able to help but Bob didn't give Alice access to the debug Permissionss. + testAccessFail(t, verror.ErrNoAccess.ID, aliceCtx, "Alice", "dm", "apps", appID, bobApp, "stats/system/pid") + + // Bob forgets that Alice can't read the stats when he can. + utiltest.VerifyGlob(t, bobCtx, "app", globtests, res) + utiltest.VerifyStatsValues(t, bobCtx, "dm", "apps", appID, bobApp, "stats/system/start-time*") + + // So Bob changes the permissions so that Alice can help debug too. + updateAccessList(t, bobCtx, "root:alice:$", string(access.Debug), "dm/apps", appID, bobApp) + + // Alice can access __debug content. + utiltest.VerifyGlob(t, aliceCtx, "app", globtestminus, res) + utiltest.VerifyFailGlob(t, aliceCtx, globtests[0:1]) + utiltest.VerifyStatsValues(t, aliceCtx, "dm", "apps", appID, bobApp, "stats", "system/start-time*") + utiltest.VerifyLog(t, aliceCtx, "dm", "apps", appID, bobApp, "logs", "*") + utiltest.VerifyPProfCmdLine(t, aliceCtx, "app", "dm", "apps", appID, bobApp, "pprof") + + // Alice can also now connect directly to the app. + utiltest.VerifyGlob(t, aliceCtx, "app", globtestminus, res) + utiltest.VerifyStatsValues(t, aliceCtx, "appV1", "__debug", "stats/system/start-time*") + + // Bob is glum because no one can help him fix his app so he terminates + // it. + utiltest.TerminateApp(t, bobCtx, appID, bobApp) + + // Cleanly shut down the device manager. + dm.Terminate(os.Interrupt) + dm.S.Expect("dm terminated") +} + +func TestClaimSetsDebugPermissions(t *testing.T) { + cleanup, ctx, sh, _, root, helperPath, idp := utiltest.StartupHelper(t) + defer cleanup() + + extraLogDir, err := ioutil.TempDir(root, "testlogs") + if err != nil { + t.Fatalf("ioutil.TempDir failed: %v", err) + } + + // Set up the device manager. + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused", "unused_curr_link") + dm.Args = append(dm.Args, "--log_dir="+extraLogDir) + dm.Start() + dm.S.Expect("READY") + + // Make some users. + selfCtx := ctx + bobCtx := utiltest.CtxWithNewPrincipal(t, selfCtx, idp, "bob") + aliceCtx := utiltest.CtxWithNewPrincipal(t, selfCtx, idp, "alice") + hjCtx := utiltest.CtxWithNewPrincipal(t, selfCtx, idp, "hackerjoe") + + // Bob claims the device manager. + utiltest.ClaimDevice(t, bobCtx, "claimable", "dm", "mydevice", utiltest.NoPairingToken) + + // Create some globbing test vectors. + dmGlobtests := []utiltest.GlobTestVector{ + {naming.Join("dm", "__debug"), "*", + []string{"http", "logs", "pprof", "stats", "vtrace"}, + }, + {naming.Join("dm", "__debug", "stats", "system"), + "start-time*", + []string{"start-time-rfc1123", "start-time-unix"}, + }, + {naming.Join("dm", "__debug", "logs"), + "*", + []string{ + // STDERR and STDOUT are not handled through the log package so + // are not included here. + "perms.test.INFO", + "perms.test.<*>.INFO.<timestamp>", + }, + }, + } + res := utiltest.NewGlobTestRegexHelper(`perms\.test`) + + // Bob claimed the DM so can access it. + utiltest.VerifyGlob(t, bobCtx, "perms.test", dmGlobtests, res) + utiltest.VerifyStatsValues(t, bobCtx, "dm", "__debug", "stats/system/start-time*") + + // Without permissions, hackerjoe can't access the device manager. + utiltest.VerifyFailGlob(t, hjCtx, dmGlobtests) + testAccessFail(t, verror.ErrNoAccess.ID, hjCtx, "hackerjoe", "dm", "__debug", "stats/system/pid") + + // Bob gives system administrator Alice admin access to the dm and hence Alice + // can access the __debug space. + updateAccessList(t, bobCtx, "root:alice:$", string(access.Admin), "dm", "device") + + // Alice is an adminstrator and so can can access device manager __debug + // values. + utiltest.VerifyGlob(t, aliceCtx, "perms.test", dmGlobtests, res) + utiltest.VerifyStatsValues(t, aliceCtx, "dm", "__debug", "stats/system/start-time*") + + // Bob gives debug access to the device manager to hackerjoe + updateAccessList(t, bobCtx, "root:hackerjoe:$", string(access.Debug), "dm", "device") + + // hackerjoe can now access the device manager + utiltest.VerifyGlob(t, hjCtx, "perms.test", dmGlobtests, res) + utiltest.VerifyStatsValues(t, hjCtx, "dm", "__debug", "stats/system/start-time*") + + // Cleanly shut down the device manager. + dm.Terminate(os.Interrupt) + dm.S.Expect("dm terminated") +} diff --git a/x/ref/services/device/deviced/internal/impl/perms/impl_test.go b/x/ref/services/device/deviced/internal/impl/perms/impl_test.go new file mode 100644 index 000000000..a6ac9adec --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/perms/impl_test.go @@ -0,0 +1,19 @@ +// Copyright 2015 The Vanadium 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 perms_test + +import ( + "testing" + + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" +) + +func TestMain(m *testing.M) { + utiltest.TestMainImpl(m) +} + +func TestSuidHelper(t *testing.T) { + utiltest.TestSuidHelperImpl(t) +} diff --git a/x/ref/services/device/deviced/internal/impl/perms/perms_test.go b/x/ref/services/device/deviced/internal/impl/perms/perms_test.go new file mode 100644 index 000000000..3bc9f6c61 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/perms/perms_test.go @@ -0,0 +1,197 @@ +// Copyright 2015 The Vanadium 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 perms_test + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "os" + "testing" + + "v.io/v23" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/device" + "v.io/v23/verror" + "v.io/x/ref/services/device/deviced/internal/impl/utiltest" + "v.io/x/ref/services/device/deviced/internal/versioning" + "v.io/x/ref/services/device/internal/errors" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" +) + +// TestDeviceManagerClaim claims a devicemanager and tests AccessList +// permissions on its methods. +func TestDeviceManagerClaim(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + // Use a common root identity provider so that all principals can talk to one + // another. + idp := testutil.NewIDProvider("root") + if err := idp.Bless(v23.GetPrincipal(ctx), "ctx"); err != nil { + t.Fatal(err) + } + + sh, deferFn := servicetest.CreateShellAndMountTable(t, ctx) + defer deferFn() + + // Set up mock application and binary repositories. + envelope, cleanup := utiltest.StartMockRepos(t, ctx) + defer cleanup() + + root, cleanup := servicetest.SetupRootDir(t, "devicemanager") + defer cleanup() + if err := versioning.SaveCreatorInfo(ctx, root); err != nil { + t.Fatal(err) + } + + // Create a script wrapping the test target that implements suidhelper. + helperPath := utiltest.GenerateSuidHelperScript(t, root) + + // Set up the device manager. Since we won't do device manager updates, + // don't worry about its application envelope and current link. + pairingToken := "abcxyz" + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link", pairingToken) + dm.Start() + dm.S.Expect("READY") + + *envelope = utiltest.EnvelopeFromShell(sh, nil, nil, utiltest.App, "google naps", 0, 0, "trapp") + + claimantCtx := utiltest.CtxWithNewPrincipal(t, ctx, idp, "claimant") + octx, err := v23.WithPrincipal(ctx, testutil.NewPrincipal("other")) + if err != nil { + t.Fatal(err) + } + + // Unclaimed devices cannot do anything but be claimed. + // TODO(ashankar,caprita): The line below will currently fail with + // ErrUnclaimedDevice != NotTrusted. NotTrusted can be avoided by + // passing options.ServerAuthorizer{security.AllowEveryone()} to the + // "Install" RPC. Refactor the helper function to make this possible. + //installAppExpectError(t, octx, impl.ErrUnclaimedDevice.ID) + + // Claim the device with an incorrect pairing token should fail. + utiltest.ClaimDeviceExpectError(t, claimantCtx, "claimable", "mydevice", "badtoken", errors.ErrInvalidPairingToken.ID) + // But succeed with a valid pairing token + utiltest.ClaimDevice(t, claimantCtx, "claimable", "dm", "mydevice", pairingToken) + + // Installation should succeed since claimantRT is now the "owner" of + // the devicemanager. + appID := utiltest.InstallApp(t, claimantCtx) + + // octx will not install the app now since it doesn't recognize the + // device's blessings. The error returned will be ErrNoServers as that + // is what the IPC stack does when there are no authorized servers. + utiltest.InstallAppExpectError(t, octx, verror.ErrNoServers.ID) + // Even if it does recognize the device (by virtue of recognizing the + // claimant), the device will not allow it to install. + claimantB, _ := v23.GetPrincipal(claimantCtx).BlessingStore().Default() + if err := security.AddToRoots(v23.GetPrincipal(octx), claimantB); err != nil { + t.Fatal(err) + } + utiltest.InstallAppExpectError(t, octx, verror.ErrNoAccess.ID) + + // Create the local server that the app uses to let us know it's ready. + pingCh, cleanup := utiltest.SetupPingServer(t, claimantCtx) + defer cleanup() + + // Start an instance of the app. + instanceID := utiltest.LaunchApp(t, claimantCtx, appID) + + // Wait until the app pings us that it's ready. + pingCh.WaitForPingArgs(t) + utiltest.Resolve(t, ctx, "trapp", 1, false) + utiltest.KillApp(t, claimantCtx, appID, instanceID) + + // TODO(gauthamt): Test that AccessLists persist across devicemanager restarts +} + +func TestDeviceManagerUpdateAccessList(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + // Use a common root identity provider so that all principals can talk to one + // another. + idp := testutil.NewIDProvider("root") + ctx = utiltest.CtxWithNewPrincipal(t, ctx, idp, "self") + + sh, deferFn := servicetest.CreateShellAndMountTable(t, ctx) + defer deferFn() + + // Set up mock application and binary repositories. + envelope, cleanup := utiltest.StartMockRepos(t, ctx) + defer cleanup() + + root, cleanup := servicetest.SetupRootDir(t, "devicemanager") + defer cleanup() + if err := versioning.SaveCreatorInfo(ctx, root); err != nil { + t.Fatal(err) + } + + selfCtx := ctx + octx := utiltest.CtxWithNewPrincipal(t, selfCtx, idp, "other") + + // Set up the device manager. Since we won't do device manager updates, + // don't worry about its application envelope and current link. + dm := utiltest.DeviceManagerCmd(sh, utiltest.DeviceManager, "dm", root, "unused_helper", "unused_app_repo_name", "unused_curr_link") + dm.Start() + dm.S.Expect("READY") + defer func() { + dm.Terminate(os.Interrupt) + dm.S.Expect("dm terminated") + utiltest.VerifyNoRunningProcesses(t) + }() + + // Create an envelope for an app. + *envelope = utiltest.EnvelopeFromShell(sh, nil, nil, utiltest.App, "google naps", 0, 0, "app") + + // On an unclaimed device manager, there will be no AccessLists. + if _, _, err := device.DeviceClient("claimable").GetPermissions(selfCtx); err == nil { + t.Fatalf("GetPermissions should have failed but didn't.") + } + + // Claim the devicemanager as "root:self:mydevice" + utiltest.ClaimDevice(t, selfCtx, "claimable", "dm", "mydevice", utiltest.NoPairingToken) + expectedAccessList := make(access.Permissions) + for _, tag := range access.AllTypicalTags() { + expectedAccessList[string(tag)] = access.AccessList{In: []security.BlessingPattern{"root:$", "root:self:$", "root:self:mydevice:$"}} + } + var b bytes.Buffer + if err := access.WritePermissions(&b, expectedAccessList); err != nil { + t.Fatalf("Failed to save AccessList:%v", err) + } + // Note, "version" below refers to the Permissions version, not the device + // manager version. + md5hash := md5.Sum(b.Bytes()) + expectedVersion := hex.EncodeToString(md5hash[:]) + deviceStub := device.DeviceClient("dm/device") + perms, version, err := deviceStub.GetPermissions(selfCtx) + if err != nil { + t.Fatal(err) + } + if version != expectedVersion { + t.Fatalf("getAccessList expected:%v(%v), got:%v(%v)", expectedAccessList, expectedVersion, perms, version) + } + // Install from octx should fail, since it does not match the AccessList. + utiltest.InstallAppExpectError(t, octx, verror.ErrNoAccess.ID) + + newAccessList := make(access.Permissions) + for _, tag := range access.AllTypicalTags() { + newAccessList.Add("root:other", string(tag)) + } + if err := deviceStub.SetPermissions(selfCtx, newAccessList, "invalid"); err == nil { + t.Fatalf("SetPermissions should have failed with invalid version") + } + if err := deviceStub.SetPermissions(selfCtx, newAccessList, version); err != nil { + t.Fatal(err) + } + // Install should now fail with selfCtx, which no longer matches the + // AccessLists but succeed with octx, which does. + utiltest.InstallAppExpectError(t, selfCtx, verror.ErrNoAccess.ID) + utiltest.InstallApp(t, octx) +} diff --git a/x/ref/services/device/deviced/internal/impl/perms_propagator.go b/x/ref/services/device/deviced/internal/impl/perms_propagator.go new file mode 100644 index 000000000..a90afdf2c --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/perms_propagator.go @@ -0,0 +1,45 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "path/filepath" + + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/x/ref/services/internal/pathperms" +) + +// computePath builds the desired path for the debug perms. +func computePath(path string) string { + return filepath.Join(path, "debugacls") +} + +// setPermsForDebugging constructs a Permissions file for use by applications +// that permits principals with a Debug right on an application instance to +// access names in the app's __debug space. +func setPermsForDebugging(blessings []string, perms access.Permissions, instancePath string, permsStore *pathperms.PathStore) error { + path := computePath(instancePath) + newPerms := make(access.Permissions) + + // Add blessings for the DM so that it can access the app too. + + set := func(bl security.BlessingPattern) { + for _, tag := range []access.Tag{access.Resolve, access.Debug} { + newPerms.Add(bl, string(tag)) + } + } + + for _, b := range blessings { + set(security.BlessingPattern(b)) + } + + // add Resolve for every blessing that has debug + for _, v := range perms["Debug"].In { + set(v) + } + _, err := permsStore.SetShareable(path, newPerms, "", true, true) + return err +} diff --git a/x/ref/services/device/deviced/internal/impl/principal_manager.go b/x/ref/services/device/deviced/internal/impl/principal_manager.go new file mode 100644 index 000000000..db96d128d --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/principal_manager.go @@ -0,0 +1,92 @@ +// Copyright 2016 The Vanadium 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 impl + +import ( + "fmt" + "os" + "path/filepath" + + "v.io/v23/security" + "v.io/x/ref/lib/exec" + vsecurity "v.io/x/ref/lib/security" + "v.io/x/ref/services/agent" +) + +// principalManager manages security principals. +type principalManager interface { + // Create creates a new principal for the instance directory. + Create(instanceDir string) error + // Delete deletes the principal for the instance directory. + Delete(instanceDir string) error + // Load returns the principal for the instance directory. Assumes the + // principal is Create-d and Serve-ing. + Load(instanceDir string) (agent.Principal, error) + // Serve makes the principal available for Load-in or for use by a child + // app. The config object, if not nil, is mutated to make the principal + // available for a child process. + Serve(instanceDir string, config exec.Config) error + // StopServing undoes Serve. + StopServing(instanceDir string) error + // Debug returns a debugging information about the principal. + Debug(instanceDir string) string +} + +func newPrincipalManager() principalManager { + return &diskCredsPM{} +} + +// diskCredsPM is a principalManager implementation that uses on-disk +// credentials. The credentials are not shared via the security agent, and are +// not locked against concurrent access. +type diskCredsPM struct{} + +type noOpClosePrincipal struct { + security.Principal +} + +func (p noOpClosePrincipal) Close() error { + return nil +} + +func (pm *diskCredsPM) Create(instanceDir string) error { + credentialsDir := filepath.Join(instanceDir, "credentials") + // TODO(caprita): The app's system user id needs access to this dir. + // Use the suidhelper to chown it. + _, err := vsecurity.CreatePersistentPrincipal(credentialsDir, nil) + return err +} + +func (pm *diskCredsPM) Delete(instanceDir string) error { + credentialsDir := filepath.Join(instanceDir, "credentials") + return os.RemoveAll(credentialsDir) +} + +func (pm *diskCredsPM) Load(instanceDir string) (agent.Principal, error) { + credentialsDir := filepath.Join(instanceDir, "credentials") + // TODO(caprita): The app's system user id needs access to this dir. + // Use the suidhelper to chown it. + p, err := vsecurity.LoadPersistentPrincipal(credentialsDir, nil) + if err != nil { + return nil, err + } + return noOpClosePrincipal{p}, nil +} + +func (pm *diskCredsPM) Serve(instanceDir string, cfg exec.Config) error { + if cfg != nil { + cfg.Set("v23.credentials", filepath.Join(instanceDir, "credentials")) + } + return nil +} + +func (pm *diskCredsPM) StopServing(instanceDir string) error { + return nil +} + +func (pm *diskCredsPM) Debug(instanceDir string) string { + credentialsDir := filepath.Join(instanceDir, "credentials") + return fmt.Sprintf("Credentials dir-based (%v)", credentialsDir) +} diff --git a/x/ref/services/device/deviced/internal/impl/profile.go b/x/ref/services/device/deviced/internal/impl/profile.go new file mode 100644 index 000000000..5c978b1cf --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/profile.go @@ -0,0 +1,196 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "bytes" + "errors" + "os/exec" + "runtime" + "strings" + + "v.io/v23/services/build" + "v.io/v23/services/device" + "v.io/x/ref/services/internal/profiles" + "v.io/x/ref/services/profile" +) + +// ComputeDeviceProfile generates a description of the runtime +// environment (supported file format, OS, architecture, libraries) of +// the host device. +// +// TODO(jsimsa): Avoid computing the host device description from +// scratch if a recent cached copy exists. +func ComputeDeviceProfile() (*profile.Specification, error) { + result := profile.Specification{} + + // Find out what the supported operating system, file format, and + // architecture is. + var os build.OperatingSystem + if err := os.SetFromGoOS(runtime.GOOS); err != nil { + return nil, err + } + result.Os = os + switch os { + case build.OperatingSystemDarwin: + result.Format = build.FormatMach + case build.OperatingSystemLinux: + result.Format = build.FormatElf + case build.OperatingSystemWindows: + result.Format = build.FormatPe + case build.OperatingSystemAndroid: + result.Format = build.FormatElf + default: + return nil, errors.New("Unsupported operating system: " + os.String()) + } + var arch build.Architecture + if err := arch.SetFromGoArch(runtime.GOARCH); err != nil { + return nil, err + } + result.Arch = arch + + // Find out what the installed dynamically linked libraries are. + switch runtime.GOOS { + case "linux": + // For Linux, we identify what dynamically linked libraries are + // installed by parsing the output of "ldconfig -p". + command := exec.Command("/sbin/ldconfig", "-p") + output, err := command.CombinedOutput() + if err != nil { + return nil, err + } + buf := bytes.NewBuffer(output) + // Throw away the first line of output from ldconfig. + if _, err := buf.ReadString('\n'); err != nil { + return nil, errors.New("Could not identify libraries.") + } + // Extract the library name and version from every subsequent line. + result.Libraries = make(map[profile.Library]struct{}) + line, err := buf.ReadString('\n') + for err == nil { + words := strings.Split(strings.Trim(line, " \t\n"), " ") + if len(words) > 0 { + tokens := strings.Split(words[0], ".so") + if len(tokens) != 2 { + return nil, errors.New("Could not identify library: " + words[0]) + } + name := strings.TrimPrefix(tokens[0], "lib") + major, minor := "", "" + tokens = strings.SplitN(tokens[1], ".", 3) + if len(tokens) >= 2 { + major = tokens[1] + } + if len(tokens) >= 3 { + minor = tokens[2] + } + result.Libraries[profile.Library{Name: name, MajorVersion: major, MinorVersion: minor}] = struct{}{} + } + line, err = buf.ReadString('\n') + } + case "darwin": + // TODO(jsimsa): Implement. + case "windows": + // TODO(jsimsa): Implement. + case "android": + // TODO(caprita): Implement. + default: + return nil, errors.New("Unsupported operating system: " + runtime.GOOS) + } + return &result, nil +} + +// getProfile gets a profile description for the given profile. +// +// TODO(jsimsa): Avoid retrieving the list of known profiles from a +// remote server if a recent cached copy exists. +func getProfile(name string) (*profile.Specification, error) { + profiles, err := profiles.GetKnownProfiles() + if err != nil { + return nil, err + } + for _, p := range profiles { + if p.Label == name { + return p, nil + } + } + return nil, nil + + // TODO(jsimsa): This function assumes the existence of a profile + // server from which the profiles can be retrieved. The profile + // server is a work in progress. When it exists, the commented out + // code below should work. + /* + var profile profile.Specification + client, err := r.NewClient() + if err != nil { + return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("NewClient() failed: %v", err)) + } + defer client.Close() + server := // TODO + method := "Specification" + inputs := make([]interface{}, 0) + call, err := client.StartCall(server + "/" + name, method, inputs) + if err != nil { + return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("StartCall(%s, %q, %v) failed: %v\n", server + "/" + name, method, inputs, err)) + } + if err := call.Finish(&profiles); err != nil { + return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("Finish(%v) failed: %v\n", &profiles, err)) + } + return &profile, nil + */ +} + +// matchProfiles inputs a profile that describes the host device and a +// set of publicly known profiles and outputs a device description that +// identifies the publicly known profiles supported by the host device. +func matchProfiles(p *profile.Specification, known []*profile.Specification) device.Description { + result := device.Description{Profiles: make(map[string]struct{})} +loop: + for _, profile := range known { + if profile.Format != p.Format { + continue + } + if profile.Os != p.Os { + continue + } + if profile.Arch != p.Arch { + continue + } + for library := range profile.Libraries { + // Current implementation requires exact library name and version match. + if _, found := p.Libraries[library]; !found { + continue loop + } + } + result.Profiles[profile.Label] = struct{}{} + } + return result +} + +// Describe returns a Description containing the profile that matches the +// current device. It's declared as a variable so we can override it for +// testing. +var Describe = func() (device.Description, error) { + empty := device.Description{} + deviceProfile, err := ComputeDeviceProfile() + if err != nil { + return empty, err + } + knownProfiles, err := profiles.GetKnownProfiles() + if err != nil { + return empty, err + } + result := matchProfiles(deviceProfile, knownProfiles) + if len(result.Profiles) == 0 { + // For now, return "unknown" as the profile, if no known profile + // matches the device's profile. + // + // TODO(caprita): Get rid of this crutch once we have profiles + // defined for our supported systems; for now it helps us make + // the integration test work on e.g. Mac. + result.Profiles["unknown"] = struct{}{} + } + return result, nil +} diff --git a/x/ref/services/device/deviced/internal/impl/proxy_invoker.go b/x/ref/services/device/deviced/internal/impl/proxy_invoker.go new file mode 100644 index 000000000..d15dec125 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/proxy_invoker.go @@ -0,0 +1,251 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "fmt" + "io" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/glob" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security/access" + "v.io/v23/vdl" + "v.io/v23/vdlroot/signature" + "v.io/v23/verror" +) + +var ( + errCantUpgradeServerCall = verror.Register(pkgPath+".errCantUpgradeServerCall", verror.NoRetry, "{1:}{2:} couldn't upgrade rpc.ServerCall to rpc.StreamServerCall{:_}") + errBadNumberOfResults = verror.Register(pkgPath+".errBadNumberOfResults", verror.NoRetry, "{1:}{2:} unexpected number of result values. Got {3}, want 2.{:_}") + errBadErrorType = verror.Register(pkgPath+".errBadErrorType", verror.NoRetry, "{1:}{2:} unexpected error type. Got {3}, want error.{:_}") + errWantSigInterfaceSlice = verror.Register(pkgPath+".errWantSigInterfaceSlice", verror.NoRetry, "{1:}{2:} unexpected result value type. Got {3}, want []signature.Interface.{:_}") + errWantSigMethod = verror.Register(pkgPath+".errWantSigMethod", verror.NoRetry, "{1:}{2:} unexpected result value type. Got {3}, want signature.Method.{:_}") + errUnknownMethod = verror.Register(pkgPath+".errUnknownMethod", verror.NoRetry, "{1:}{2:} unknown method{:_}") +) + +// proxyInvoker is an rpc.Invoker implementation that proxies all requests +// to a remote object, i.e. requests to <suffix> are forwarded to +// <remote> transparently. +// +// remote is the name of the remote object. +// access is the access tag require to access the object. +// desc is used to determine the number of results for a given method. +func newProxyInvoker(remote string, access access.Tag, desc []rpc.InterfaceDesc) *proxyInvoker { + methodNumResults := make(map[string]int) + for _, iface := range desc { + for _, method := range iface.Methods { + methodNumResults[method.Name] = len(method.OutArgs) + } + } + return &proxyInvoker{remote, access, methodNumResults} +} + +type proxyInvoker struct { + remote string + access access.Tag + methodNumResults map[string]int +} + +var _ rpc.Invoker = (*proxyInvoker)(nil) + +func (p *proxyInvoker) Prepare(_ *context.T, method string, numArgs int) (argptrs []interface{}, tags []*vdl.Value, _ error) { + // TODO(toddw): Change argptrs to be filled in with *vdl.Value, to avoid + // unnecessary type lookups. + argptrs = make([]interface{}, numArgs) + for i, _ := range argptrs { + var x interface{} + argptrs[i] = &x + } + tags = []*vdl.Value{vdl.ValueOf(p.access)} + return +} + +func (p *proxyInvoker) Invoke(ctx *context.T, inCall rpc.StreamServerCall, method string, argptrs []interface{}) (results []interface{}, err error) { + // We accept any values as argument and pass them through to the remote + // server. + args := make([]interface{}, len(argptrs)) + for i, ap := range argptrs { + args[i] = ap + } + client := v23.GetClient(ctx) + + outCall, err := client.StartCall(ctx, p.remote, method, args) + if err != nil { + return nil, err + } + + // Each RPC has a bi-directional stream, and there is no way to know in + // advance how much data will be sent in either direction, if any. + // + // This method (Invoke) must return when the remote server is done with + // the RPC, which is when outCall.Recv() returns EOF. When that happens, + // we need to call outCall.Finish() to get the return values, and then + // return these values to the client. + // + // While we are forwarding data from the server to the client, we must + // also forward data from the client to the server. This happens in a + // separate goroutine. This goroutine may return after Invoke has + // returned if the client doesn't call CloseSend() explicitly. + // + // Any error, other than EOF, will be returned to the client, if + // possible. The only situation where it is not possible to send an + // error to the client is when the error comes from forwarding data from + // the client to the server and Invoke has already returned or is about + // to return. In this case, the error is lost. So, it is possible that + // the client could successfully Send() data that the server doesn't + // actually receive if the server terminates the RPC while the data is + // in the proxy. + fwd := func(src, dst rpc.Stream, errors chan<- error) { + for { + var obj interface{} + switch err := src.Recv(&obj); err { + case io.EOF: + if call, ok := src.(rpc.ClientCall); ok { + if err := call.CloseSend(); err != nil { + errors <- err + } + } + return + case nil: + break + default: + errors <- err + return + } + if err := dst.Send(obj); err != nil { + errors <- err + return + } + } + } + errors := make(chan error, 2) + go fwd(inCall, outCall, errors) + fwd(outCall, inCall, errors) + select { + case err := <-errors: + return nil, err + default: + } + + nResults, err := p.numResults(ctx, method) + if err != nil { + return nil, err + } + + // We accept any return values, without type checking, and return them + // to the client. + res := make([]interface{}, nResults) + for i := 0; i < len(res); i++ { + var foo interface{} + res[i] = &foo + } + err = outCall.Finish(res...) + results = make([]interface{}, len(res)) + for i, r := range res { + results[i] = *r.(*interface{}) + } + return results, err +} + +// TODO(toddw): Expose a helper function that performs all error checking based +// on reflection, to simplify the repeated logic processing results. +func (p *proxyInvoker) Signature(ctx *context.T, call rpc.ServerCall) ([]signature.Interface, error) { + streamCall, ok := call.(rpc.StreamServerCall) + if !ok { + return nil, verror.New(errCantUpgradeServerCall, ctx) + } + results, err := p.Invoke(ctx, streamCall, rpc.ReservedSignature, nil) + if err != nil { + return nil, err + } + if len(results) != 2 { + return nil, verror.New(errBadNumberOfResults, ctx, len(results)) + } + if results[1] != nil { + err, ok := results[1].(error) + if !ok { + return nil, verror.New(errBadErrorType, ctx, fmt.Sprintf("%T", err)) + } + return nil, err + } + var res []signature.Interface + if results[0] != nil { + sig, ok := results[0].([]signature.Interface) + if !ok { + return nil, verror.New(errWantSigInterfaceSlice, ctx, fmt.Sprintf("%T", sig)) + } + } + return res, nil +} + +func (p *proxyInvoker) MethodSignature(ctx *context.T, call rpc.ServerCall, method string) (signature.Method, error) { + empty := signature.Method{} + streamCall, ok := call.(rpc.StreamServerCall) + if !ok { + return empty, verror.New(errCantUpgradeServerCall, ctx) + } + results, err := p.Invoke(ctx, streamCall, rpc.ReservedMethodSignature, []interface{}{&method}) + if err != nil { + return empty, err + } + if len(results) != 2 { + return empty, verror.New(errBadNumberOfResults, ctx, len(results)) + } + if results[1] != nil { + err, ok := results[1].(error) + if !ok { + return empty, verror.New(errBadErrorType, ctx, fmt.Sprintf("%T", err)) + } + return empty, err + } + var res signature.Method + if results[0] != nil { + sig, ok := results[0].(signature.Method) + if !ok { + return empty, verror.New(errWantSigMethod, ctx, fmt.Sprintf("%T", sig)) + } + } + return res, nil +} + +func (p *proxyInvoker) Globber() *rpc.GlobState { + return &rpc.GlobState{AllGlobber: p} +} + +type call struct { + rpc.GlobServerCall +} + +func (c *call) Recv(v interface{}) error { + return io.EOF +} + +func (c *call) Send(v interface{}) error { + return c.SendStream().Send(v.(naming.GlobReply)) +} + +func (p *proxyInvoker) Glob__(ctx *context.T, serverCall rpc.GlobServerCall, g *glob.Glob) error { + pattern := g.String() + p.Invoke(ctx, &call{serverCall}, rpc.GlobMethod, []interface{}{&pattern}) + return nil +} + +// numResults returns the number of result values for the given method. +func (p *proxyInvoker) numResults(ctx *context.T, method string) (int, error) { + switch method { + case rpc.GlobMethod: + return 1, nil + case rpc.ReservedSignature, rpc.ReservedMethodSignature: + return 2, nil + } + num, ok := p.methodNumResults[method] + if !ok { + return 0, verror.New(errUnknownMethod, ctx, method) + } + return num, nil +} diff --git a/x/ref/services/device/deviced/internal/impl/proxy_invoker_test.go b/x/ref/services/device/deviced/internal/impl/proxy_invoker_test.go new file mode 100644 index 000000000..225027e2c --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/proxy_invoker_test.go @@ -0,0 +1,78 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "reflect" + "testing" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + libstats "v.io/v23/services/stats" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" +) + +// TODO(toddw): Add tests of Signature and MethodSignature. + +func TestProxyInvoker(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + // server1 is a normal server + ctx, server1, err := v23.WithNewServer(ctx, "", &dummy{}, nil) + if err != nil { + t.Fatalf("NewServer: %v", err) + } + + // server2 proxies requests to <suffix> to server1/__debug/stats/<suffix> + disp := &proxyDispatcher{ + remote: naming.JoinAddressName(server1.Status().Endpoints[0].String(), "__debug/stats"), + desc: libstats.StatsServer(nil).Describe__(), + } + ctx, server2, err := v23.WithNewDispatchingServer(ctx, "", disp) + if err != nil { + t.Fatalf("NewServer: %v", err) + } + addr2 := server2.Status().Endpoints[0].String() + + // Call Value() + name := naming.JoinAddressName(addr2, "system/start-time-rfc1123") + c := libstats.StatsClient(name) + if _, err := c.Value(ctx); err != nil { + t.Fatalf("%q.Value() error: %v", name, err) + } + + // Call Glob() + results, _, err := testutil.GlobName(ctx, naming.JoinAddressName(addr2, "system"), "start-time-*") + if err != nil { + t.Fatalf("Glob failed: %v", err) + } + expected := []string{ + "start-time-rfc1123", + "start-time-unix", + } + if !reflect.DeepEqual(results, expected) { + t.Errorf("unexpected results. Got %q, want %q", results, expected) + } +} + +type dummy struct{} + +func (*dummy) Method(*context.T, rpc.ServerCall) error { return nil } + +type proxyDispatcher struct { + remote string + desc []rpc.InterfaceDesc +} + +func (d *proxyDispatcher) Lookup(ctx *context.T, suffix string) (interface{}, security.Authorizer, error) { + ctx.Infof("LOOKUP(%s): remote .... %s", suffix, d.remote) + return newProxyInvoker(naming.Join(d.remote, suffix), access.Debug, d.desc), nil, nil +} diff --git a/x/ref/services/device/deviced/internal/impl/restart_policy.go b/x/ref/services/device/deviced/internal/impl/restart_policy.go new file mode 100644 index 000000000..dc4a8af86 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/restart_policy.go @@ -0,0 +1,55 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "time" + + "v.io/v23/services/application" +) + +// RestartPolicy instances provide a policy for deciding if an +// application should be restarted on failure. +type restartPolicy interface { + // decide determines if this application instance should be (re)started, returning + // true if the application should be be (re)started. + decide(envelope *application.Envelope, instance *instanceInfo) bool +} + +type basicDecisionPolicy struct { +} + +func newBasicRestartPolicy() restartPolicy { + return new(basicDecisionPolicy) +} + +func (rp *basicDecisionPolicy) decide(envelope *application.Envelope, instance *instanceInfo) bool { + if envelope.Restarts == 0 { + return false + } + if envelope.Restarts < 0 { + return true + } + + if instance.Restarts == 0 { + instance.RestartWindowBegan = time.Now() + } + + endOfWindow := instance.RestartWindowBegan.Add(envelope.RestartTimeWindow) + if time.Now().After(endOfWindow) { + instance.Restarts = 1 + instance.RestartWindowBegan = time.Now() + return true + } + + if instance.Restarts < envelope.Restarts { + instance.Restarts++ + instance.RestartWindowBegan = time.Now() + return true + } + + return false + +} diff --git a/x/ref/services/device/deviced/internal/impl/restart_policy_test.go b/x/ref/services/device/deviced/internal/impl/restart_policy_test.go new file mode 100644 index 000000000..0ccf7ad46 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/restart_policy_test.go @@ -0,0 +1,135 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "testing" + "time" + + "v.io/v23/services/application" +) + +// TestRestartPolicy verifies that the daemon mode restart policy operates +// as intended. +func TestRestartPolicy(t *testing.T) { + nbr := newBasicRestartPolicy() + + type tV struct { + envelope *application.Envelope + info *instanceInfo + wantInfo *instanceInfo + decision bool + } + + testNow := time.Now() + + testVectors := []tV{ + // -1 means always restart. + { + &application.Envelope{ + Restarts: -1, + }, + &instanceInfo{ + Restarts: 0, + }, + &instanceInfo{ + Restarts: 0, + }, + true, + }, + // 0 means restart exactly 0 times. + { + &application.Envelope{ + Restarts: 0, + }, + &instanceInfo{ + Restarts: 0, + }, + &instanceInfo{ + Restarts: 0, + }, + false, + }, + // 1 means restart once (2 invocations total) + { + &application.Envelope{ + Restarts: 1, + RestartTimeWindow: time.Hour, + }, + &instanceInfo{ + Restarts: 0, + }, + &instanceInfo{ + Restarts: 1, + RestartWindowBegan: time.Now(), + }, + true, + }, + // but only ever once. + { + &application.Envelope{ + Restarts: 1, + RestartTimeWindow: time.Hour, + }, + &instanceInfo{ + Restarts: 1, + RestartWindowBegan: testNow, + }, + &instanceInfo{ + Restarts: 1, + RestartWindowBegan: testNow, + }, + false, + }, + // after time window, restart count is reset. + { + &application.Envelope{ + Restarts: 1, + RestartTimeWindow: time.Minute, + }, + &instanceInfo{ + Restarts: 1, + RestartWindowBegan: time.Now().Add(-time.Hour), + }, + &instanceInfo{ + Restarts: 1, + RestartWindowBegan: time.Now(), + }, + true, + }, + // Every time a restart happens, the beginning of the window + // should be reset. + { + &application.Envelope{ + Restarts: 2, + RestartTimeWindow: time.Minute, + }, + &instanceInfo{ + Restarts: 1, + RestartWindowBegan: time.Now().Add(-10 * time.Second), + }, + &instanceInfo{ + Restarts: 2, + RestartWindowBegan: time.Now(), + }, + true, + }, + } + + for ti, tv := range testVectors { + if got, want := nbr.decide(tv.envelope, tv.info), tv.decision; got != want { + t.Errorf("Test case #%d: basicDecisionPolicy decide: got %v, want %v", ti, got, want) + } + + if got, want := tv.info.Restarts, tv.wantInfo.Restarts; got != want { + t.Errorf("basicDecisionPolicy instanceInfo Restarts update got %v, want %v", got, want) + } + + // Times should be "nearly" same. + if got, want := tv.info.RestartWindowBegan, tv.wantInfo.RestartWindowBegan; !((got.Sub(want) < time.Second) && (got.Sub(want) >= 0)) { + t.Errorf("Test case #%d: basicDecisionPolicy instanceInfo RestartTimeBegan got %v, want %v", ti, got, want) + } + } +} diff --git a/x/ref/services/device/deviced/internal/impl/shell_android.go b/x/ref/services/device/deviced/internal/impl/shell_android.go new file mode 100644 index 000000000..f45cf69be --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/shell_android.go @@ -0,0 +1,10 @@ +// Copyright 2015 The Vanadium 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 impl + +const ( + ShellPath = "/system/bin/sh" + DateCommand = "/system/bin/date +%s.$RANDOM" +) diff --git a/x/ref/services/device/deviced/internal/impl/shell_darwin.go b/x/ref/services/device/deviced/internal/impl/shell_darwin.go new file mode 100644 index 000000000..e1b54a92a --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/shell_darwin.go @@ -0,0 +1,10 @@ +// Copyright 2015 The Vanadium 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 impl + +const ( + ShellPath = "/bin/bash" + DateCommand = "/bin/date +%s.$RANDOM" +) diff --git a/x/ref/services/device/deviced/internal/impl/shell_linux.go b/x/ref/services/device/deviced/internal/impl/shell_linux.go new file mode 100644 index 000000000..3b81a07f2 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/shell_linux.go @@ -0,0 +1,12 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build linux,!android + +package impl + +const ( + ShellPath = "/bin/bash" + DateCommand = "/bin/date +%s%N" +) diff --git a/x/ref/services/device/deviced/internal/impl/stats.go b/x/ref/services/device/deviced/internal/impl/stats.go new file mode 100644 index 000000000..36bb8b4d3 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/stats.go @@ -0,0 +1,68 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "sync" + + "v.io/v23/naming" + libstats "v.io/x/ref/lib/stats" + "v.io/x/ref/lib/stats/counter" +) + +// stats contains various exported stats we maintain for the device manager. +type stats struct { + sync.Mutex + // Prefix for each of the stat names. + prefix string + // How many times apps were run via the Run rpc. + runs *counter.Counter + // How many times apps were auto-restarted by the reaper. + restarts *counter.Counter + // Same as above, but broken down by instance. + // This is not of type lib/stats::Map since we want the detailed rate + // and delta stats per instance. + // TODO(caprita): Garbage-collect old instances? + runsPerInstance map[string]*counter.Counter + restartsPerInstance map[string]*counter.Counter +} + +func newCounter(names ...string) *counter.Counter { + return libstats.NewCounter(naming.Join(names...)) +} + +func newStats(prefix string) *stats { + return &stats{ + runs: newCounter(prefix, "runs"), + runsPerInstance: make(map[string]*counter.Counter), + restarts: newCounter(prefix, "restarts"), + restartsPerInstance: make(map[string]*counter.Counter), + prefix: prefix, + } +} + +func (s *stats) incrRestarts(instance string) { + s.Lock() + defer s.Unlock() + s.restarts.Incr(1) + perInstanceCtr, ok := s.restartsPerInstance[instance] + if !ok { + perInstanceCtr = newCounter(s.prefix, "restarts", instance) + s.restartsPerInstance[instance] = perInstanceCtr + } + perInstanceCtr.Incr(1) +} + +func (s *stats) incrRuns(instance string) { + s.Lock() + defer s.Unlock() + s.runs.Incr(1) + perInstanceCtr, ok := s.runsPerInstance[instance] + if !ok { + perInstanceCtr = newCounter(s.prefix, "runs", instance) + s.runsPerInstance[instance] = perInstanceCtr + } + perInstanceCtr.Incr(1) +} diff --git a/x/ref/services/device/deviced/internal/impl/tidyup.go b/x/ref/services/device/deviced/internal/impl/tidyup.go new file mode 100644 index 000000000..f9cdfc008 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/tidyup.go @@ -0,0 +1,257 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "v.io/v23/context" + "v.io/v23/services/device" + "v.io/v23/verror" + "v.io/x/ref/services/device/internal/errors" +) + +// This file contains the various routines that the device manager uses +// to tidy up its persisted but no longer necessary state. + +const aboutOneDay = time.Hour * 24 + +func oldEnoughToTidy(fi os.FileInfo, now time.Time) bool { + return fi.ModTime().Add(aboutOneDay).Before(now) +} + +// AutomaticTidyingInterval defaults to 1 day. +// Settable for tests. +var AutomaticTidyingInterval = time.Hour * 24 + +func shouldDelete(idir, suffix string, now time.Time) (bool, error) { + fi, err := os.Stat(filepath.Join(idir, suffix)) + if err != nil { + return false, err + } + + return oldEnoughToTidy(fi, now), nil +} + +// Exposed for replacability in tests. +var MockableNow = time.Now + +// shouldDeleteInstallation returns true if the tidying policy holds +// for this installation. +func shouldDeleteInstallation(idir string, now time.Time) (bool, error) { + return shouldDelete(idir, device.InstallationStateUninstalled.String(), now) +} + +// shouldDeleteInstance returns true if the tidying policy holds +// that the instance should be deleted. +func shouldDeleteInstance(idir string, now time.Time) (bool, error) { + return shouldDelete(idir, device.InstanceStateDeleted.String(), now) +} + +type pthError struct { + pth string + err error +} + +func pruneDeletedInstances(ctx *context.T, root string, now time.Time) error { + paths, err := filepath.Glob(filepath.Join(root, "app*", "installation*", "instances", "instance*")) + if err != nil { + return err + } + + allerrors := make([]pthError, 0) + + for _, pth := range paths { + state, err := getInstanceState(pth) + if err != nil { + allerrors = append(allerrors, pthError{pth, err}) + continue + } + if state != device.InstanceStateDeleted { + continue + } + + shouldDelete, err := shouldDeleteInstance(pth, now) + if err != nil { + allerrors = append(allerrors, pthError{pth, err}) + continue + } + + if shouldDelete { + if err := suidHelper.deleteFileTree(ctx, pth, nil, nil); err != nil { + allerrors = append(allerrors, pthError{pth, err}) + } + } + } + return processErrors(ctx, allerrors) +} + +func processErrors(ctx *context.T, allerrors []pthError) error { + if len(allerrors) > 0 { + errormessages := make([]string, 0, len(allerrors)) + for _, ep := range allerrors { + errormessages = append(errormessages, fmt.Sprintf("path: %s failed: %v", ep.pth, ep.err)) + } + return verror.New(errors.ErrOperationFailed, ctx, "Some older instances could not be deleted: %s", strings.Join(errormessages, ", ")) + } + return nil +} + +func pruneUninstalledInstallations(ctx *context.T, root string, now time.Time) error { + // Read all the Uninstalled installations into a map. + installationPaths, err := filepath.Glob(filepath.Join(root, "app*", "installation*")) + if err != nil { + return err + } + pruneCandidates := make(map[string]struct{}, len(installationPaths)) + for _, p := range installationPaths { + state, err := getInstallationState(p) + if err != nil { + return err + } + + if state != device.InstallationStateUninstalled { + continue + } + + pruneCandidates[p] = struct{}{} + } + + instancePaths, err := filepath.Glob(filepath.Join(root, "app*", "installation*", "instances", "instance*", "installation")) + if err != nil { + return err + } + + allerrors := make([]pthError, 0) + + // Filter out installations that are still owned by an instance. Note + // that pruneUninstalledInstallations runs after + // pruneDeletedInstances so that freshly-pruned Instances will not + // retain the Installation. + for _, idir := range instancePaths { + installPath, err := os.Readlink(idir) + if err != nil { + allerrors = append(allerrors, pthError{idir, err}) + continue + } + + if _, ok := pruneCandidates[installPath]; ok { + delete(pruneCandidates, installPath) + } + } + + // All remaining entries in pruneCandidates are not referenced by + // any instance. + for pth, _ := range pruneCandidates { + shouldDelete, err := shouldDeleteInstallation(pth, now) + if err != nil { + allerrors = append(allerrors, pthError{pth, err}) + continue + } + + if shouldDelete { + if err := suidHelper.deleteFileTree(ctx, pth, nil, nil); err != nil { + allerrors = append(allerrors, pthError{pth, err}) + } + } + } + return processErrors(ctx, allerrors) +} + +// pruneOldLogs removes logs more than a day old. Symlinks (the +// cannonical log file name) the (newest) log files that they point to +// are preserved. +func pruneOldLogs(ctx *context.T, root string, now time.Time) error { + logPaths, err := filepath.Glob(filepath.Join(root, "app*", "installation*", "instances", "instance*", "logs", "*")) + if err != nil { + return err + } + + pruneCandidates := make(map[string]struct{}, len(logPaths)) + for _, p := range logPaths { + pruneCandidates[p] = struct{}{} + } + + allerrors := make([]pthError, 0) + for p, _ := range pruneCandidates { + fi, err := os.Stat(p) + if err != nil { + allerrors = append(allerrors, pthError{p, err}) + delete(pruneCandidates, p) + continue + } + + if fi.Mode()&os.ModeSymlink != 0 { + delete(pruneCandidates, p) + target, err := os.Readlink(p) + if err != nil { + allerrors = append(allerrors, pthError{p, err}) + continue + } + delete(pruneCandidates, target) + continue + } + + if !oldEnoughToTidy(fi, now) { + delete(pruneCandidates, p) + } + } + + for pth, _ := range pruneCandidates { + if err := suidHelper.deleteFileTree(ctx, pth, nil, nil); err != nil { + allerrors = append(allerrors, pthError{pth, err}) + } + } + return processErrors(ctx, allerrors) +} + +// tidyHarness runs device manager cleanup operations +func tidyHarness(ctx *context.T, root string) error { + now := MockableNow() + + if err := pruneDeletedInstances(ctx, root, now); err != nil { + return err + } + + if err := pruneUninstalledInstallations(ctx, root, now); err != nil { + return err + } + + return pruneOldLogs(ctx, root, now) +} + +// tidyDaemon runs in a Go routine, processing requests to tidy +// or tidying on a schedule. +func tidyDaemon(ctx *context.T, c <-chan tidyRequests, root string) { + for { + select { + case req, ok := <-c: + if !ok { + return + } + req.bc <- tidyHarness(req.ctx, root) + case <-time.After(AutomaticTidyingInterval): + if err := tidyHarness(nil, root); err != nil { + ctx.Errorf("tidyDaemon failed to tidy: %v", err) + } + } + + } +} + +type tidyRequests struct { + ctx *context.T + bc chan<- error +} + +func newTidyingDaemon(ctx *context.T, root string) chan<- tidyRequests { + c := make(chan tidyRequests) + go tidyDaemon(ctx, c, root) + return c +} diff --git a/x/ref/services/device/deviced/internal/impl/util.go b/x/ref/services/device/deviced/internal/impl/util.go new file mode 100644 index 000000000..8bd75f95b --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/util.go @@ -0,0 +1,249 @@ +// Copyright 2015 The Vanadium 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 impl + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "reflect" + "regexp" + "strings" + "time" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/security" + "v.io/v23/services/application" + "v.io/v23/services/repository" + "v.io/v23/verror" + "v.io/x/ref/services/device/internal/config" + "v.io/x/ref/services/device/internal/errors" + "v.io/x/ref/services/internal/binarylib" +) + +// TODO(caprita): Set these timeout in a more principled manner. +const ( + childReadyTimeout = 40 * time.Second + childWaitTimeout = 40 * time.Second + rpcContextTimeout = time.Minute + rpcContextLongTimeout = 5 * time.Minute +) + +func verifySignature(data []byte, publisher security.Blessings, sig security.Signature) error { + if !publisher.IsZero() { + h := sha256.Sum256(data) + if !sig.Verify(publisher.PublicKey(), h[:]) { + return verror.New(errors.ErrOperationFailed, nil) + } + } + return nil +} + +func downloadBinary(ctx *context.T, publisher security.Blessings, bin *application.SignedFile, workspace, fileName string) error { + // TODO(gauthamt): Reduce the number of passes we make over the binary/package + // data to verify its checksum and signature. + data, _, err := binarylib.Download(ctx, bin.File) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Download(%v) failed: %v", bin.File, err)) + } + if err := verifySignature(data, publisher, bin.Signature); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Publisher binary(%v) signature verification failed", bin.File)) + } + path, perm := filepath.Join(workspace, fileName), os.FileMode(0755) + if err := ioutil.WriteFile(path, data, perm); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("WriteFile(%v, %v) failed: %v", path, perm, err)) + } + return nil +} + +// TODO(caprita): share code between downloadBinary and downloadPackages. +func downloadPackages(ctx *context.T, publisher security.Blessings, packages application.Packages, pkgDir string) error { + for localPkg, pkgName := range packages { + if localPkg == "" || localPkg[0] == '.' || strings.Contains(localPkg, string(filepath.Separator)) { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("invalid local package name: %q", localPkg)) + } + path := filepath.Join(pkgDir, localPkg) + if err := binarylib.DownloadToFile(ctx, pkgName.File, path); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("DownloadToFile(%q, %q) failed: %v", pkgName, path, err)) + } + data, err := ioutil.ReadFile(path) + if err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("ReadPackage(%v) failed: %v", path, err)) + } + // If a nonempty signature is present, verify it. (i.e., we accept unsigned packages.) + if !reflect.DeepEqual(pkgName.Signature, security.Signature{}) { + if err := verifySignature(data, publisher, pkgName.Signature); err != nil { + return verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Publisher package(%v:%v) signature verification failed", localPkg, pkgName)) + } + } + } + return nil +} + +func fetchEnvelope(ctx *context.T, origin string) (*application.Envelope, error) { + stub := repository.ApplicationClient(origin) + profilesSet, err := Describe() + if err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Failed to obtain profile labels: %v", err)) + } + var profiles []string + for label := range profilesSet.Profiles { + profiles = append(profiles, label) + } + envelope, err := stub.Match(ctx, profiles) + if err != nil { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("Match(%v) failed: %v", profiles, err)) + } + // If a publisher blessing is present, it must be from a publisher we recognize. If not, + // reject the envelope. Note that unsigned envelopes are accepted by this check. + // TODO: Implment a real ACL check based on publisher + names, rejected := publisherBlessingNames(ctx, envelope) + if len(names) == 0 && len(rejected) > 0 { + return nil, verror.New(errors.ErrOperationFailed, ctx, fmt.Sprintf("publisher %v in envelope %v was not recognized", rejected, envelope.Title)) + } + return &envelope, nil +} + +func publisherBlessingNames(ctx *context.T, env application.Envelope) ([]string, []security.RejectedBlessing) { + p := v23.GetPrincipal(ctx) + b, _ := p.BlessingStore().Default() + call := security.NewCall(&security.CallParams{ + RemoteBlessings: env.Publisher, + LocalBlessings: b, + LocalPrincipal: p, + Timestamp: time.Now(), + }) + names, rejected := security.RemoteBlessingNames(ctx, call) + if len(rejected) > 0 { + ctx.Infof("For envelope %v, rejected publisher blessings: %v", env.Title, rejected) + } + ctx.VI(2).Infof("accepted publisher blessings: %v", names) + return names, rejected +} + +// LinkSelf creates a link to the current binary. +func LinkSelf(workspace, fileName string) error { + path := filepath.Join(workspace, fileName) + self := os.Args[0] + if err := os.Link(self, path); err != nil { + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("Link(%v, %v) failed: %v", self, path, err)) + } + return nil +} + +func generateVersionDirName() string { + // TODO(caprita): Use generateID instead. + return time.Now().Format(time.RFC3339Nano) +} + +func UpdateLink(target, link string) error { + newLink := link + ".new" + fi, err := os.Lstat(newLink) + if err == nil { + if err := os.Remove(fi.Name()); err != nil { + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("Remove(%v) failed: %v", fi.Name(), err)) + } + } + if err := os.Symlink(target, newLink); err != nil { + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("Symlink(%v, %v) failed: %v", target, newLink, err)) + } + if err := os.Rename(newLink, link); err != nil { + return verror.New(errors.ErrOperationFailed, nil, fmt.Sprintf("Rename(%v, %v) failed: %v", newLink, link, err)) + } + return nil +} + +func BaseCleanupDir(ctx *context.T, path, helper string) { + if helper != "" { + out, err := exec.Command(helper, "--rm", path).CombinedOutput() + if err != nil { + ctx.Errorf("exec.Command(%s %s %s).CombinedOutput() failed: %v", helper, "--rm", path, err) + return + } + if len(out) != 0 { + ctx.Errorf("exec.Command(%s %s %s).CombinedOutput() generated output: %v", helper, "--rm", path, string(out)) + } + } else { + if err := os.RemoveAll(path); err != nil { + ctx.Errorf("RemoveAll(%v) failed: %v", path, err) + } + } +} + +func PermsDir(c *config.State) string { + return filepath.Join(c.Root, "device-manager", "device-data", "acls") +} + +// CleanupDir is defined like this so we can override its implementation for +// tests. CleanupDir will use the helper to delete application state possibly +// owned by different accounts if helper is provided. +var CleanupDir = BaseCleanupDir + +// VanadiumEnvironment returns only the environment variables that are specific +// to the Vanadium system. +func VanadiumEnvironment(env []string) []string { + return filterEnvironment(env, allowedVarsRE, deniedVarsRE) +} + +var allowedVarsRE = regexp.MustCompile("^(V23_.*|GOSH_.*|PAUSE_BEFORE_STOP|TMPDIR|PATH)$") + +var deniedVarsRE = regexp.MustCompile("^(V23_EXEC_VERSION|V23_EXEC_CONFIG)$") + +// filterEnvironment returns only the environment variables, specified by +// the env parameter, whose names match the supplied regexp. +func filterEnvironment(env []string, allow, deny *regexp.Regexp) []string { + var ret []string + for _, e := range env { + if eqIdx := strings.Index(e, "="); eqIdx > 0 { + key := e[:eqIdx] + if deny.MatchString(key) { + continue + } + if allow.MatchString(key) { + ret = append(ret, e) + } + } + } + return ret +} + +// generateRandomString returns a cryptographically-strong random string. +func generateRandomString() (string, error) { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(b), nil +} + +// generateAgentSockDir returns the name of a newly created directory where to +// create an agent socket. +func generateAgentSockDir(rootDir string) (string, error) { + randomPattern, err := generateRandomString() + if err != nil { + return "", err + } + // We keep the socket files close to the root dir of the device + // manager installation to ensure that the socket file path is + // shorter than 108 characters (a requirement on Linux). + sockDir := filepath.Join(rootDir, "socks", randomPattern) + // TODO(caprita): For multi-user mode, we should chown the + // socket dir to the app user, and set up a unix group to permit + // access to the socket dir to the agent and device manager. + // For now, 'security' hinges on the fact that the name of the + // socket dir is unknown to everyone except the device manager, + // the agent, and the app. + if err := os.MkdirAll(sockDir, 0711); err != nil { + return "", fmt.Errorf("MkdirAll(%q) failed: %v", sockDir, err) + } + return sockDir, nil +} diff --git a/x/ref/services/device/deviced/internal/impl/utiltest/app.go b/x/ref/services/device/deviced/internal/impl/utiltest/app.go new file mode 100644 index 000000000..9a9f615b4 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/utiltest/app.go @@ -0,0 +1,204 @@ +// Copyright 2015 The Vanadium 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 utiltest + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/x/lib/gosh" + "v.io/x/lib/vlog" + "v.io/x/ref/lib/exec" + "v.io/x/ref/lib/mgmt" + "v.io/x/ref/lib/signals" + "v.io/x/ref/services/device/internal/suid" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" +) + +const ( + TestFlagName = "random_test_flag" +) + +var flagValue = flag.String(TestFlagName, "default", "") + +func init() { + // The installer sets this flag on the installed device manager, so we + // need to ensure it's defined. + flag.String("name", "", "") +} + +// appService defines a test service that the test app should be running. +// TODO(caprita): Use this to make calls to the app and verify how Kill +// interacts with an active service. +type appService struct{} + +func (appService) Echo(_ *context.T, _ rpc.ServerCall, message string) (string, error) { + return message, nil +} + +func (appService) Cat(_ *context.T, _ rpc.ServerCall, file string) (string, error) { + if file == "" || file[0] == filepath.Separator || file[0] == '.' { + return "", fmt.Errorf("illegal file name: %q", file) + } + bytes, err := ioutil.ReadFile(file) + if err != nil { + return "", err + } + return string(bytes), nil +} + +type PingArgs struct { + Username, FlagValue, EnvValue, + DefaultPeerBlessings, PubBlessingPrefixes, InstanceName string + Pid int +} + +// ping makes an RPC from the App back to the invoking device manager +// carrying a PingArgs instance. +func ping(ctx *context.T, flagValue string) { + ctx.Errorf("ping flagValue: %s", flagValue) + + helperEnv := os.Getenv(suid.SavedArgs) + d := json.NewDecoder(strings.NewReader(helperEnv)) + var savedArgs suid.ArgsSavedForTest + if err := d.Decode(&savedArgs); err != nil { + ctx.Fatalf("Failed to decode preserved argument %v: %v", helperEnv, err) + } + args := &PingArgs{ + // TODO(rjkroege): Consider validating additional parameters + // from helper. + Username: savedArgs.Uname, + FlagValue: flagValue, + EnvValue: os.Getenv(TestEnvVarName), + Pid: os.Getpid(), + DefaultPeerBlessings: v23.GetPrincipal(ctx).BlessingStore().ForPeer("nonexistent").String(), + } + + config, err := exec.ReadConfigFromOSEnv() + if config == nil || err != nil { + vlog.Fatalf("Couldn't get Config: %v", err) + } else { + args.PubBlessingPrefixes, _ = config.Get(mgmt.PublisherBlessingPrefixesKey) + args.InstanceName, _ = config.Get(mgmt.InstanceNameKey) + } + + client := v23.GetClient(ctx) + if call, err := client.StartCall(ctx, "pingserver", "Ping", []interface{}{args}); err != nil { + ctx.Fatalf("StartCall failed: %v", err) + } else if err := call.Finish(); err != nil { + ctx.Fatalf("Finish failed: %v", err) + } +} + +// Cat is an RPC invoked from the test harness process to the application +// process. +func Cat(ctx *context.T, name, file string) (string, error) { + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + client := v23.GetClient(ctx) + call, err := client.StartCall(ctx, name, "Cat", []interface{}{file}) + if err != nil { + return "", err + } + var content string + if err := call.Finish(&content); err != nil { + return "", err + } + return content, nil +} + +// App is a test application. It pings the invoking device manager with state +// information. +var App = gosh.RegisterFunc("App", appFunc) + +func appFunc(publishName string) error { + ctx, shutdown := test.V23Init() + defer shutdown() + + ctx, server, err := v23.WithNewServer(ctx, publishName, new(appService), nil) + if err != nil { + ctx.Fatalf("NewServer(%v) failed: %v", publishName, err) + } + WaitForMount(ctx, ctx, publishName, server) + // Some of our tests look for log files, so make sure they are flushed + // to ensure that at least the files exist. + ctx.FlushLog() + + shutdownChan := signals.ShutdownOnSignals(ctx) + ping(ctx, *flagValue) + + <-shutdownChan + if err := ioutil.WriteFile("testfile", []byte("goodbye world"), 0600); err != nil { + ctx.Fatalf("Failed to write testfile: %v", err) + } + return nil +} + +type PingServer struct { + ing chan PingArgs +} + +// TODO(caprita): Set the timeout in a more principled manner. +const pingTimeout = time.Minute + +func (p PingServer) Ping(_ *context.T, _ rpc.ServerCall, arg PingArgs) error { + p.ing <- arg + return nil +} + +// SetupPingServer creates a server listening for a ping from a child app; it +// returns a channel on which the app's ping message is returned, and a cleanup +// function. +func SetupPingServer(t *testing.T, ctx *context.T) (PingServer, func()) { + pingCh := make(chan PingArgs, 1) + ctx, cancel := context.WithCancel(ctx) + ctx, server, err := v23.WithNewServer(ctx, "pingserver", PingServer{pingCh}, security.AllowEveryone()) + if err != nil { + t.Fatalf("NewServer(%q, <dispatcher>) failed: %v", "pingserver", err) + } + WaitForMount(t, ctx, "pingserver", server) + return PingServer{pingCh}, func() { + cancel() + <-server.Closed() + } +} + +func (p PingServer) WaitForPingArgs(t *testing.T) PingArgs { + var args PingArgs + select { + case args = <-p.ing: + case <-time.After(pingTimeout): + t.Fatal(testutil.FormatLogLine(2, "failed to get ping")) + } + return args +} + +func (p PingServer) VerifyPingArgs(t *testing.T, username, flagValue, envValue string) PingArgs { + args := p.WaitForPingArgs(t) + if args.Username != username || args.FlagValue != flagValue || args.EnvValue != envValue { + t.Fatal(testutil.FormatLogLine(2, "got ping args %v, expected [username = %v, flag value = %v, env value = %v]", args, username, flagValue, envValue)) + } + return args // Useful for tests that want to check other values in the PingArgs result. +} + +// HangingApp is the same as App, except that it does not exit properly after +// being stopped. +var HangingApp = gosh.RegisterFunc("HangingApp", func(publishName string) error { + err := appFunc(publishName) + time.Sleep(24 * time.Hour) + return err +}) diff --git a/x/ref/services/device/deviced/internal/impl/utiltest/helpers.go b/x/ref/services/device/deviced/internal/impl/utiltest/helpers.go new file mode 100644 index 000000000..e49788217 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/utiltest/helpers.go @@ -0,0 +1,908 @@ +// Copyright 2015 The Vanadium 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 utiltest + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "os/user" + "path/filepath" + "reflect" + "regexp" + "sort" + "strings" + "syscall" + "testing" + "time" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/options" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/services/application" + "v.io/v23/services/device" + "v.io/v23/services/logreader" + "v.io/v23/services/pprof" + "v.io/v23/services/stats" + "v.io/v23/verror" + "v.io/x/lib/envvar" + "v.io/x/lib/gosh" + "v.io/x/ref" + "v.io/x/ref/internal/logger" + vsecurity "v.io/x/ref/lib/security" + _ "v.io/x/ref/runtime/factories/roaming" + "v.io/x/ref/services/device/deviced/internal/impl" + "v.io/x/ref/services/device/deviced/internal/versioning" + "v.io/x/ref/services/internal/binarylib" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" + "v.io/x/ref/test/v23test" +) + +const ( + // TODO(caprita): Set the timeout in a more principled manner. + killTimeout = 20 * time.Second +) + +func init() { + impl.Describe = func() (descr device.Description, err error) { + return device.Description{Profiles: map[string]struct{}{"test-profile": struct{}{}}}, nil + } + + impl.CleanupDir = func(ctx *context.T, dir, helper string) { + if dir == "" { + return + } + parentDir, base := filepath.Dir(dir), filepath.Base(dir) + var renamed string + if helper != "" { + renamed = filepath.Join(parentDir, "helper_deleted_"+base) + } else { + renamed = filepath.Join(parentDir, "deleted_"+base) + } + if err := os.Rename(dir, renamed); err != nil { + ctx.Errorf("Rename(%v, %v) failed: %v", dir, renamed, err) + } + } + + // Return a sequence of times separated by 25 hours. + impl.MockableNow = func() time.Time { + now := time.Now() + impl.MockableNow = func() time.Time { + now = now.Add(time.Hour * 25) + return now + } + return now + } + +} + +func EnvelopeFromShell(sh *v23test.Shell, vars, flags []string, f *gosh.Func, title string, retries int, window time.Duration, args ...interface{}) application.Envelope { + c := sh.FuncCmd(f, args...) + // Make sure the command is not provided with credentials from the shell; + // device manager is responsible for providing it credentials. + delete(c.Vars, ref.EnvCredentials) + delete(c.Vars, ref.EnvAgentPath) + // Note, vars is allowed to contain credentials env vars that were set + // deliberately. + c.Vars = envvar.MergeMaps(c.Vars, envvar.SliceToMap(vars)) + // Configure the command to not exit when its parent exits, since device + // manager starts commands using a "suid helper" subprocess that exits + // immediately. + c.IgnoreParentExit = true + c.ExitAfter = time.Minute // make sure the child exits eventually + c.Args = append(c.Args, flags...) + return application.Envelope{ + Title: title, + Args: c.Args[1:], + // TODO(caprita): revisit how the environment is sanitized for arbirary + // apps. + Env: impl.VanadiumEnvironment(envvar.MapToSlice(c.Vars)), + Binary: application.SignedFile{File: MockBinaryRepoName}, + Restarts: int32(retries), + RestartTimeWindow: window, + } +} + +func SignedEnvelopeFromShell(ctx *context.T, sh *v23test.Shell, vars, flags []string, f *gosh.Func, title string, retries int, window time.Duration, args ...interface{}) (application.Envelope, error) { + envelope := EnvelopeFromShell(sh, vars, flags, f, title, retries, window, args...) + reader, cleanup, err := mockBinaryBytesReader() + defer cleanup() + sig, err := binarylib.Sign(ctx, reader) + if err != nil { + return application.Envelope{}, err + } + envelope.Binary.Signature = *sig + + // Add a publisher blessing + p := v23.GetPrincipal(ctx) + b, _ := p.BlessingStore().Default() + publisher, err := p.Bless(p.PublicKey(), b, "angryapp.v10", security.UnconstrainedUse()) + if err != nil { + return application.Envelope{}, err + } + envelope.Publisher = publisher + return envelope, nil +} + +// ResolveExpectNotFound verifies that the given name is not in the mounttable. +func ResolveExpectNotFound(f Fatalist, ctx *context.T, name string, retry bool) { + expectErr := naming.ErrNoSuchName.ID + for { + me, err := v23.GetNamespace(ctx).Resolve(ctx, name) + if err == nil || verror.ErrorID(err) != expectErr { + if retry { + time.Sleep(10 * time.Millisecond) + continue + } + if err == nil { + f.Fatal(testutil.FormatLogLine(2, "Resolve(%v) succeeded with results %v when it was expected to fail", name, me.Names())) + } else { + f.Fatal(testutil.FormatLogLine(2, "Resolve(%v) failed with error %v, expected error ID %v", name, err, expectErr)) + } + } else { + return + } + } +} + +// Resolve looks up the given name in the mounttable. +func Resolve(f Fatalist, ctx *context.T, name string, expectReplicas int, retry bool) []string { + for { + me, err := v23.GetNamespace(ctx).Resolve(ctx, name) + if err != nil { + if retry { + time.Sleep(10 * time.Millisecond) + continue + } else { + f.Fatalf("Resolve(%v) failed: %v", name, err) + } + } + + // We are going to get a websocket and a tcp endpoint for each + // replica. Filter out non-tcp endpoints. + filteredResults := []string{} + for _, r := range me.Names() { + if strings.Index(r, "@tcp") != -1 { + filteredResults = append(filteredResults, r) + } + } + if want, got := expectReplicas, len(filteredResults); want != got { + f.Fatalf("Resolve(%v) expected %d result(s), got %d instead", name, want, got) + } + return filteredResults + } +} + +// The following set of functions are convenience wrappers around Update and +// Revert for device manager. + +func DeviceStub(name string) device.DeviceClientMethods { + deviceName := naming.Join(name, "device") + return device.DeviceClient(deviceName) +} + +func ClaimDevice(t *testing.T, ctx *context.T, claimableName, deviceName, extension, pairingToken string) { + // Setup blessings to be granted to the claimed device + g := &granter{extension: extension} + s := options.ServerAuthorizer{security.AllowEveryone()} + // Call the Claim RPC: Skip server authorization because the unclaimed + // device presents nothing that can be used to recognize it. + if err := device.ClaimableClient(claimableName).Claim(ctx, pairingToken, g, s); err != nil { + t.Fatal(testutil.FormatLogLine(2, "%q.Claim(%q) failed: %v [%v]", claimableName, pairingToken, verror.ErrorID(err), err)) + } + // Wait for the device to remount itself with the device service after + // being claimed. + start := time.Now() + for { + _, err := v23.GetNamespace(ctx).Resolve(ctx, deviceName) + if err == nil { + return + } + ctx.VI(4).Infof("Resolve(%q) failed: %v", err) + time.Sleep(time.Millisecond) + if elapsed := time.Since(start); elapsed > time.Minute { + t.Fatal(testutil.FormatLogLine(2, "Device hasn't remounted itself in %v since it was claimed", elapsed)) + } + } +} + +func ClaimDeviceExpectError(t *testing.T, ctx *context.T, name, extension, pairingToken string, errID verror.ID) { + // Setup blessings to be granted to the claimed device + g := &granter{extension: extension} + s := options.ServerAuthorizer{security.AllowEveryone()} + // Call the Claim RPC + if err := device.ClaimableClient(name).Claim(ctx, pairingToken, g, s); verror.ErrorID(err) != errID { + t.Fatal(testutil.FormatLogLine(2, "%q.Claim(%q) expected to fail with %v, got %v [%v]", name, pairingToken, errID, verror.ErrorID(err), err)) + } +} + +func UpdateDeviceExpectError(t *testing.T, ctx *context.T, name string, errID verror.ID) { + if err := DeviceStub(name).Update(ctx); verror.ErrorID(err) != errID { + t.Fatal(testutil.FormatLogLine(2, "%q.Update expected to fail with %v, got %v [%v]", name, errID, verror.ErrorID(err), err)) + } +} + +func UpdateDevice(t *testing.T, ctx *context.T, name string) { + if err := DeviceStub(name).Update(ctx); err != nil { + t.Fatal(testutil.FormatLogLine(2, "%q.Update() failed: %v [%v]", name, verror.ErrorID(err), err)) + } +} + +func RevertDeviceExpectError(t *testing.T, ctx *context.T, name string, errID verror.ID) { + if err := DeviceStub(name).Revert(ctx); verror.ErrorID(err) != errID { + t.Fatal(testutil.FormatLogLine(2, "%q.Revert() expected to fail with %v, got %v [%v]", name, errID, verror.ErrorID(err), err)) + } +} + +func RevertDevice(t *testing.T, ctx *context.T, name string) { + if err := DeviceStub(name).Revert(ctx); err != nil { + t.Fatal(testutil.FormatLogLine(2, "%q.Revert() failed: %v [%v]", name, verror.ErrorID(err), err)) + } +} + +func KillDevice(t *testing.T, ctx *context.T, name string) { + if err := DeviceStub(name).Kill(ctx, killTimeout); err != nil { + t.Fatal(testutil.FormatLogLine(2, "%q.Kill(%v) failed: %v [%v]", name, killTimeout, verror.ErrorID(err), err)) + } +} + +func ShutdownDevice(t *testing.T, ctx *context.T, name string) { + if err := DeviceStub(name).Delete(ctx); err != nil { + t.Fatal(testutil.FormatLogLine(2, "%q.Delete() failed: %v [%v]", name, verror.ErrorID(err), err)) + } +} + +// The following set of functions are convenience wrappers around various app +// management methods. + +func Ocfg(opt []interface{}) device.Config { + for _, o := range opt { + if c, ok := o.(device.Config); ok { + return c + } + } + return device.Config{} +} + +func Opkg(opt []interface{}) application.Packages { + for _, o := range opt { + if c, ok := o.(application.Packages); ok { + return c + } + } + return application.Packages{} +} + +func AppStub(nameComponents ...string) device.ApplicationClientMethods { + appsName := "dm/apps" + appName := naming.Join(append([]string{appsName}, nameComponents...)...) + return device.ApplicationClient(appName) +} + +func StatsStub(nameComponents ...string) stats.StatsClientMethods { + statsName := naming.Join(nameComponents...) + return stats.StatsClient(statsName) +} + +func InstallApp(t *testing.T, ctx *context.T, opt ...interface{}) string { + appID, err := AppStub().Install(ctx, MockApplicationRepoName, Ocfg(opt), Opkg(opt)) + if err != nil { + t.Fatal(testutil.FormatLogLine(2, "Install failed: %v [%v]", verror.ErrorID(err), err)) + } + return appID +} + +func InstallAppExpectError(t *testing.T, ctx *context.T, expectedError verror.ID, opt ...interface{}) { + if _, err := AppStub().Install(ctx, MockApplicationRepoName, Ocfg(opt), Opkg(opt)); err == nil || verror.ErrorID(err) != expectedError { + t.Fatal(testutil.FormatLogLine(2, "Install expected to fail with %v, got %v [%v]", expectedError, verror.ErrorID(err), err)) + } +} + +type granter struct { + rpc.CallOpt + p security.Principal + extension string +} + +func (g *granter) Grant(ctx *context.T, call security.Call) (security.Blessings, error) { + p := call.LocalPrincipal() + b, _ := p.BlessingStore().Default() + return p.Bless(call.RemoteBlessings().PublicKey(), b, g.extension, security.UnconstrainedUse()) +} + +func LaunchAppImpl(t *testing.T, ctx *context.T, appID, grant string) (string, error) { + instanceID, err := NewInstanceImpl(t, ctx, appID, grant) + if err != nil { + return "", err + } + return instanceID, AppStub(appID, instanceID).Run(ctx) +} + +func NewInstanceImpl(t *testing.T, ctx *context.T, appID, grant string) (string, error) { + call, err := AppStub(appID).Instantiate(ctx) + if err != nil { + return "", err + } + // We should finish the rpc call, even if we exit early due to an error. + defer call.Finish() + + for call.RecvStream().Advance() { + switch msg := call.RecvStream().Value().(type) { + case device.BlessServerMessageInstancePublicKey: + p := v23.GetPrincipal(ctx) + b, _ := p.BlessingStore().Default() + pubKey, err := security.UnmarshalPublicKey(msg.Value) + if err != nil { + return "", err + } + blessings, err := p.Bless(pubKey, b, grant, security.UnconstrainedUse()) + if err != nil { + return "", errors.New("bless failed") + } + call.SendStream().Send(device.BlessClientMessageAppBlessings{Value: blessings}) + default: + return "", fmt.Errorf("newInstanceImpl: received unexpected message: %#v", msg) + } + } + var instanceID string + if instanceID, err = call.Finish(); err != nil { + return "", err + } + return instanceID, nil +} + +func LaunchApp(t *testing.T, ctx *context.T, appID string) string { + instanceID, err := LaunchAppImpl(t, ctx, appID, "forapp") + if err != nil { + t.Fatal(testutil.FormatLogLine(2, "launching %v failed: %v [%v]", appID, verror.ErrorID(err), err)) + } + return instanceID +} + +func LaunchAppExpectError(t *testing.T, ctx *context.T, appID string, expectedError verror.ID) { + if _, err := LaunchAppImpl(t, ctx, appID, "forapp"); err == nil || verror.ErrorID(err) != expectedError { + t.Fatal(testutil.FormatLogLine(2, "launching %v expected to fail with %v, got %v [%v]", appID, expectedError, verror.ErrorID(err), err)) + } +} + +func TerminateApp(t *testing.T, ctx *context.T, appID, instanceID string) { + if err := AppStub(appID, instanceID).Kill(ctx, killTimeout); err != nil { + t.Fatal(testutil.FormatLogLine(2, "Kill(%v/%v) failed: %v [%v]", appID, instanceID, verror.ErrorID(err), err)) + } + if err := AppStub(appID, instanceID).Delete(ctx); err != nil { + t.Fatal(testutil.FormatLogLine(2, "Delete(%v/%v) failed: %v [%v]", appID, instanceID, verror.ErrorID(err), err)) + } +} + +func KillApp(t *testing.T, ctx *context.T, appID, instanceID string) { + if err := AppStub(appID, instanceID).Kill(ctx, killTimeout); err != nil { + t.Fatal(testutil.FormatLogLine(2, "Kill(%v/%v) failed: %v [%v]", appID, instanceID, verror.ErrorID(err), err)) + } +} + +func DeleteApp(t *testing.T, ctx *context.T, appID, instanceID string) { + if err := AppStub(appID, instanceID).Delete(ctx); err != nil { + t.Fatal(testutil.FormatLogLine(2, "Delete(%v/%v) failed: %v [%v]", appID, instanceID, verror.ErrorID(err), err)) + } +} + +func RunApp(t *testing.T, ctx *context.T, appID, instanceID string) { + if err := AppStub(appID, instanceID).Run(ctx); err != nil { + t.Fatal(testutil.FormatLogLine(2, "Run(%v/%v) failed: %v [%v]", appID, instanceID, verror.ErrorID(err), err)) + } +} + +func RunAppExpectError(t *testing.T, ctx *context.T, appID, instanceID string, expectedError verror.ID) { + if err := AppStub(appID, instanceID).Run(ctx); err == nil || verror.ErrorID(err) != expectedError { + t.Fatal(testutil.FormatLogLine(2, "Run(%v/%v) expected to fail with %v, got %v [%v]", appID, instanceID, expectedError, verror.ErrorID(err), err)) + } +} + +func UpdateInstance(t *testing.T, ctx *context.T, appID, instanceID string) { + if err := AppStub(appID, instanceID).Update(ctx); err != nil { + t.Fatal(testutil.FormatLogLine(2, "Update(%v/%v) failed: %v [%v]", appID, instanceID, verror.ErrorID(err), err)) + } +} + +func UpdateInstanceExpectError(t *testing.T, ctx *context.T, appID, instanceID string, expectedError verror.ID) { + if err := AppStub(appID, instanceID).Update(ctx); err == nil || verror.ErrorID(err) != expectedError { + t.Fatal(testutil.FormatLogLine(2, "Update(%v/%v) expected to fail with %v, got %v [%v]", appID, instanceID, expectedError, verror.ErrorID(err), err)) + } +} + +func UpdateApp(t *testing.T, ctx *context.T, appID string) { + if err := AppStub(appID).Update(ctx); err != nil { + t.Fatal(testutil.FormatLogLine(2, "Update(%v) failed: %v [%v]", appID, verror.ErrorID(err), err)) + } +} + +func UpdateAppExpectError(t *testing.T, ctx *context.T, appID string, expectedError verror.ID) { + if err := AppStub(appID).Update(ctx); err == nil || verror.ErrorID(err) != expectedError { + t.Fatal(testutil.FormatLogLine(2, "Update(%v) expected to fail with %v, got %v [%v]", appID, expectedError, verror.ErrorID(err), err)) + } +} + +func RevertApp(t *testing.T, ctx *context.T, appID string) { + if err := AppStub(appID).Revert(ctx); err != nil { + t.Fatal(testutil.FormatLogLine(2, "Revert(%v) failed: %v [%v]", appID, verror.ErrorID(err), err)) + } +} + +func RevertAppExpectError(t *testing.T, ctx *context.T, appID string, expectedError verror.ID) { + if err := AppStub(appID).Revert(ctx); err == nil || verror.ErrorID(err) != expectedError { + t.Fatal(testutil.FormatLogLine(2, "Revert(%v) expected to fail with %v, got %v [%v]", appID, expectedError, verror.ErrorID(err), err)) + } +} + +func UninstallApp(t *testing.T, ctx *context.T, appID string) { + if err := AppStub(appID).Uninstall(ctx); err != nil { + t.Fatal(testutil.FormatLogLine(2, "Uninstall(%v) failed: %v [%v]", appID, verror.ErrorID(err), err)) + } +} + +func Debug(t *testing.T, ctx *context.T, nameComponents ...string) string { + dbg, err := AppStub(nameComponents...).Debug(ctx) + if err != nil { + t.Fatal(testutil.FormatLogLine(2, "Debug(%v) failed: %v [%v]", nameComponents, verror.ErrorID(err), err)) + } + return dbg +} + +func VerifyDeviceState(t *testing.T, ctx *context.T, want device.InstanceState, name string) string { + s, err := DeviceStub(name).Status(ctx) + if err != nil { + t.Fatal(testutil.FormatLogLine(2, "Status(%v) failed: %v [%v]", name, verror.ErrorID(err), err)) + } + status, ok := s.(device.StatusDevice) + if !ok { + t.Fatal(testutil.FormatLogLine(2, "Status(%v) returned unknown type: %T", name, s)) + } + if status.Value.State != want { + t.Fatal(testutil.FormatLogLine(2, "Status(%v) state: wanted %v, got %v", name, want, status.Value.State)) + } + return status.Value.Version +} + +func Status(t *testing.T, ctx *context.T, nameComponents ...string) device.Status { + s, err := AppStub(nameComponents...).Status(ctx) + if err != nil { + t.Error(testutil.FormatLogLine(3, "Status(%v) failed: %v [%v]", nameComponents, verror.ErrorID(err), err)) + } + return s +} + +func VerifyState(t *testing.T, ctx *context.T, want interface{}, nameComponents ...string) string { + s := Status(t, ctx, nameComponents...) + var ( + state interface{} + version string + ) + switch s := s.(type) { + case device.StatusInstance: + state = s.Value.State + version = s.Value.Version + case device.StatusInstallation: + state = s.Value.State + version = s.Value.Version + default: + t.Error(testutil.FormatLogLine(2, "Status(%v) returned unknown type: %T", nameComponents, s)) + } + if state != want { + t.Error(testutil.FormatLogLine(2, "Status(%v) state: wanted %v (%T), got %v (%T)", nameComponents, want, want, state, state)) + } + return version +} + +func WaitForState(t *testing.T, ctx *context.T, want interface{}, nameComponents ...string) { + timeOut := time.After(30 * time.Second) + for { + s, err := AppStub(nameComponents...).Status(ctx) + // err may be non-nil when the app state cannot be determined. + // This can happen as a result of + // getInstanceState/getInstallationState not being thread-safe + // when the app state is changing. For such cases, just retry. + if err == nil { + var state interface{} + switch s := s.(type) { + case device.StatusInstance: + state = s.Value.State + case device.StatusInstallation: + state = s.Value.State + default: + t.Error(testutil.FormatLogLine(2, "Status(%v) returned unknown type: %T", nameComponents, s)) + } + if state == want { + return + } + } + select { + case <-timeOut: + t.Fatal(testutil.FormatLogLine(2, "Timed out waiting for %v to reach state %v", nameComponents, want)) + case <-time.After(time.Millisecond): + // Try again. + } + } +} + +// Code to make Association lists sortable. +type byIdentity []device.Association + +func (a byIdentity) Len() int { return len(a) } +func (a byIdentity) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byIdentity) Less(i, j int) bool { return a[i].IdentityName < a[j].IdentityName } + +func CompareAssociations(t *testing.T, got, expected []device.Association) { + sort.Sort(byIdentity(got)) + sort.Sort(byIdentity(expected)) + if !reflect.DeepEqual(got, expected) { + t.Errorf("ListAssociations() got %v, expected %v", got, expected) + } +} + +// GenerateSuidHelperScript builds a script to execute the test target as +// a suidhelper instance and returns the path to the script. +func GenerateSuidHelperScript(t *testing.T, root string) string { + output := "#!" + impl.ShellPath + "\n" + output += "V23_SUIDHELPER_TEST=1" + output += " " + output += "exec " + os.Args[0] + " -minuid=1 -test.run=TestSuidHelper \"$@\"" + output += "\n" + + logger.Global().VI(1).Infof("script\n%s", output) + + if err := os.MkdirAll(root, 0755); err != nil { + t.Fatal(testutil.FormatLogLine(2, "MkdirAll failed: %v", err)) + } + path := filepath.Join(root, "helper.sh") + if err := ioutil.WriteFile(path, []byte(output), 0755); err != nil { + t.Fatal(testutil.FormatLogLine(2, "WriteFile(%v) failed: %v", path, err)) + } + return path +} + +// GenerateRestarter creates a simple script that acts as the restarter +// for tests. It blackholes arguments meant for the restarter. +func GenerateRestarter(t *testing.T, root string) string { + output := "#!" + impl.ShellPath + "\n" + + ` +ARGS=$* +for ARG in ${ARGS[@]}; do + if [[ ${ARG} = -- ]]; then + ARGS=(${ARGS[@]/$ARG}) + break + elif [[ ${ARG} == --* ]]; then + ARGS=(${ARGS[@]/$ARG}) + else + break + fi +done + +exec ${ARGS[@]} +` + if err := os.MkdirAll(root, 0755); err != nil { + t.Fatal(testutil.FormatLogLine(2, "MkdirAll failed: %v", err)) + } + path := filepath.Join(root, "agenthelper.sh") + if err := ioutil.WriteFile(path, []byte(output), 0755); err != nil { + t.Fatal(testutil.FormatLogLine(2, "WriteFile(%v) failed: %v", path, err)) + } + return path +} + +func CtxWithNewPrincipal(t *testing.T, ctx *context.T, idp *testutil.IDProvider, extension string) *context.T { + ret, err := v23.WithPrincipal(ctx, testutil.NewPrincipal()) + if err != nil { + t.Fatal(testutil.FormatLogLine(2, "v23.WithPrincipal failed: %v", err)) + } + if err := idp.Bless(v23.GetPrincipal(ret), extension); err != nil { + t.Fatal(testutil.FormatLogLine(2, "idp.Bless(?, %q) failed: %v", extension, err)) + } + return ret +} + +// CreatePrincipal sets up a principal in a temporary directory (to be cleaned +// up by the shell at the end) and returns that directory. +func CreatePrincipal(t *testing.T, sh *v23test.Shell) string { + principalDir := sh.MakeTempDir() + if _, err := vsecurity.CreatePersistentPrincipal(principalDir, nil); err != nil { + t.Fatal(err) + } + return principalDir +} + +// TODO(rjkroege): This helper is generally useful. Use it to reduce +// boilerplate across all device manager tests. +func StartupHelper(t *testing.T) (func(), *context.T, *v23test.Shell, *application.Envelope, string, string, *testutil.IDProvider) { + ctx, shutdown := test.V23Init() + + // Make a new identity context. + idp := testutil.NewIDProvider("root") + ctx = CtxWithNewPrincipal(t, ctx, idp, "self") + + sh, deferFn := servicetest.CreateShellAndMountTable(t, ctx) + + // Set up mock application and binary repositories. + envelope, envCleanup := StartMockRepos(t, ctx) + + root, rootCleanup := servicetest.SetupRootDir(t, "devicemanager") + if err := versioning.SaveCreatorInfo(ctx, root); err != nil { + t.Fatal(testutil.FormatLogLine(2, "SaveCreatorInfo failed: %v", err)) + } + + // Create a script wrapping the test target that implements suidhelper. + helperPath := GenerateSuidHelperScript(t, root) + + return func() { + rootCleanup() + envCleanup() + deferFn() + shutdown() + }, ctx, sh, envelope, root, helperPath, idp +} + +type GlobTestVector struct { + Name, Pattern string + Expected []string +} + +type globTestRegexHelper struct { + logFileTimeStampRE *regexp.Regexp + logFileTrimInfoRE *regexp.Regexp + logFileRemoveErrorFatalWarningRE *regexp.Regexp + statsTrimRE *regexp.Regexp +} + +func NewGlobTestRegexHelper(appName string) *globTestRegexHelper { + return &globTestRegexHelper{ + logFileTimeStampRE: regexp.MustCompile("(STDOUT|STDERR)-[0-9]+$"), + logFileTrimInfoRE: regexp.MustCompile(appName + `\..*\.INFO\.[0-9.-]+$`), + logFileRemoveErrorFatalWarningRE: regexp.MustCompile("(ERROR|FATAL|WARNING)"), + statsTrimRE: regexp.MustCompile("/stats/(rpc|system(/start-time.*)?)$"), + } +} + +// VerifyGlob verifies that for each GlobTestVector instance that the +// pattern returns the expected matches. +func VerifyGlob(t *testing.T, ctx *context.T, appName string, testcases []GlobTestVector, res *globTestRegexHelper) { + for _, tc := range testcases { + results, _, err := testutil.GlobName(ctx, tc.Name, tc.Pattern) + if err != nil { + t.Error(testutil.FormatLogLine(2, "unexpected glob error for (%q, %q): %v", tc.Name, tc.Pattern, err)) + continue + } + filteredResults := []string{} + for _, name := range results { + // Keep only the stats object names that match this RE. + if strings.Contains(name, "/stats/") && !res.statsTrimRE.MatchString(name) { + continue + } + // Remove ERROR, WARNING, FATAL log files because + // they're not consistently there. + if res.logFileRemoveErrorFatalWarningRE.MatchString(name) { + continue + } + name = res.logFileTimeStampRE.ReplaceAllString(name, "$1-<timestamp>") + name = res.logFileTrimInfoRE.ReplaceAllString(name, appName+".<*>.INFO.<timestamp>") + filteredResults = append(filteredResults, name) + } + sort.Strings(filteredResults) + sort.Strings(tc.Expected) + if !reflect.DeepEqual(filteredResults, tc.Expected) { + t.Error(testutil.FormatLogLine(2, "unexpected result for (%q, %q). Got %q, want %q", tc.Name, tc.Pattern, filteredResults, tc.Expected)) + } + } +} + +// VerifyFailGlob verifies that for each GlobTestVector instance that the +// pattern returns no matches. +func VerifyFailGlob(t *testing.T, ctx *context.T, testcases []GlobTestVector) { + for _, tc := range testcases { + results, _, _ := testutil.GlobName(ctx, tc.Name, tc.Pattern) + if len(results) != 0 { + t.Error(testutil.FormatLogLine(2, "verifyFailGlob should have failed for %q, %q", tc.Name, tc.Pattern)) + } + } +} + +// VerifyLog calls Size() on a selection of log file objects to +// demonstrate that the log files are accessible and have been written by +// the application. +func VerifyLog(t *testing.T, ctx *context.T, nameComponents ...string) { + a := nameComponents + pattern, prefix := a[len(a)-1], a[:len(a)-1] + path := naming.Join(prefix...) + files, _, err := testutil.GlobName(ctx, path, pattern) + if err != nil { + t.Error(testutil.FormatLogLine(2, "unexpected glob error: %v", err)) + } + if want, got := 4, len(files); got < want { + t.Error(testutil.FormatLogLine(2, "Unexpected number of matches. Got %d, want at least %d", got, want)) + } + for _, file := range files { + name := naming.Join(path, file) + c := logreader.LogFileClient(name) + if _, err := c.Size(ctx); err != nil { + t.Error(testutil.FormatLogLine(2, "Size(%q) failed: %v", name, err)) + } + } +} + +// VerifyStatsValues call Value() on some of the stats objects to prove +// that they are correctly being proxied to the device manager. +func VerifyStatsValues(t *testing.T, ctx *context.T, nameComponents ...string) { + a := nameComponents + pattern, prefix := a[len(a)-1], a[:len(a)-1] + path := naming.Join(prefix...) + objects, _, err := testutil.GlobName(ctx, path, pattern) + + if err != nil { + t.Error(testutil.FormatLogLine(2, "unexpected glob error: %v", err)) + } + if want, got := 2, len(objects); got != want { + t.Error(testutil.FormatLogLine(2, "Unexpected number of matches. Got %d, want %d", got, want)) + } + for _, obj := range objects { + name := naming.Join(path, obj) + c := stats.StatsClient(name) + if _, err := c.Value(ctx); err != nil { + t.Error(testutil.FormatLogLine(2, "Value(%q) failed: %v", name, err)) + } + } +} + +// VerifyPProfCmdLine calls CmdLine() on the pprof object to validate +// that it the proxy correctly accessess pprof names. +func VerifyPProfCmdLine(t *testing.T, ctx *context.T, appName string, nameComponents ...string) { + name := naming.Join(nameComponents...) + c := pprof.PProfClient(name) + v, err := c.CmdLine(ctx) + if err != nil { + t.Error(testutil.FormatLogLine(2, "CmdLine(%q) failed: %v", name, err)) + } + if len(v) == 0 { + t.Errorf("Unexpected empty cmdline: %v", v) + } + if got, want := filepath.Base(v[0]), appName; got != want { + t.Error(testutil.FormatLogLine(2, "Unexpected value for argv[0]. Got %v, want %v", got, want)) + } + +} + +func VerifyNoRunningProcesses(t *testing.T) { + if impl.RunningChildrenProcesses() { + t.Errorf("device manager incorrectly terminating with child processes still running") + } +} + +func SetNamespaceRootsForUnclaimedDevice(ctx *context.T) (*context.T, error) { + origroots := v23.GetNamespace(ctx).Roots() + roots := make([]string, len(origroots)) + for i, orig := range origroots { + addr, suffix := naming.SplitAddressName(orig) + origep, err := naming.ParseEndpoint(addr) + if err != nil { + return nil, err + } + ep := naming.FormatEndpoint( + origep.Addr().Network(), + origep.Addr().String(), + origep.RoutingID, + naming.ServesMountTable(origep.ServesMountTable)) + roots[i] = naming.JoinAddressName(ep, suffix) + } + ctx.Infof("Changing namespace roots from %v to %v", origroots, roots) + ctx, _, err := v23.WithNewNamespace(ctx, roots...) + return ctx, err +} + +func UserName(t *testing.T) string { + u, err := user.Current() + if err != nil { + t.Fatal(testutil.FormatLogLine(2, "user.Current() failed: %v", err)) + } + return u.Username +} + +func StartRealBinaryRepository(t *testing.T, ctx *context.T, von string) func() { + rootDir, err := binarylib.SetupRootDir("") + if err != nil { + t.Fatal(testutil.FormatLogLine(2, "binarylib.SetupRootDir failed: %v", err)) + } + state, err := binarylib.NewState(rootDir, "", 3) + if err != nil { + t.Fatal(testutil.FormatLogLine(2, "binarylib.NewState failed: %v", err)) + } + d, err := binarylib.NewDispatcher(ctx, state) + if err != nil { + t.Fatal(testutil.FormatLogLine(2, "server.NewDispatcher failed: %v", err)) + } + ctx, cancel := context.WithCancel(ctx) + ctx, server, err := v23.WithNewDispatchingServer(ctx, von, d) + if err != nil { + t.Fatal(testutil.FormatLogLine(2, "server.ServeDispatcher failed: %v", err)) + } + WaitForMount(t, ctx, von, server) + return func() { + cancel() + <-server.Closed() + if err := os.RemoveAll(rootDir); err != nil { + t.Fatal(testutil.FormatLogLine(2, "os.RemoveAll(%q) failed: %v", rootDir, err)) + } + } +} + +func GetPid(t *testing.T, ctx *context.T, appID, instanceID string) int { + name := naming.Join("dm", "apps/"+appID+"/"+instanceID+"/stats/system/pid") + c := stats.StatsClient(name) + v, err := c.Value(ctx) + if err != nil { + t.Fatal(testutil.FormatLogLine(2, "Value() failed: %v", err)) + } + var pid int + if err := v.ToValue(&pid); err != nil { + t.Fatal(testutil.FormatLogLine(2, "ToValue() failed: %v", err)) + } + return pid +} + +// PollingWait polls a given process to make sure that it has exited +// before continuing or fails with a time-out. +func PollingWait(t *testing.T, pid int) { + timeOut := time.After(30 * time.Second) + for syscall.Kill(pid, 0) == nil { + select { + case <-timeOut: + syscall.Kill(pid, 9) + t.Fatal(testutil.FormatLogLine(2, "Timed out waiting for PID %v to terminate", pid)) + case <-time.After(time.Millisecond): + // Try again. + } + } +} + +type Fatalist interface { + Fatal(args ...interface{}) + Fatalf(format string, args ...interface{}) +} + +// WaitForMount waits (with a reasonable timeout) for the given server's +// endpoint to be mounted under the given name. Since ctx.WithNewServer and +// ctx.WithNewDispatchingServer publish the names asynchronously, use +// WaitForMount if you need a guarantee that the publish has happened. +func WaitForMount(f Fatalist, ctx *context.T, name string, server rpc.Server) { + var serverEPName string + if serverEPS := server.Status().Endpoints; len(serverEPS) != 1 { + f.Fatalf("Expected 1 server endpoint, found: %v", serverEPS) + } else { + serverEPName = naming.JoinAddressName(serverEPS[0].String(), "") + } + timeout := time.After(time.Minute) + for { + // NOTE(caprita): We could have also used server.Status().Mounts + // to save the trouble of resolving the mounttable, but given + // the churn in the server logic, it's safer to just look for + // the 'ground truth' in the mounttable. + eps, err := v23.GetNamespace(ctx).Resolve(ctx, name) + if err == nil { + for _, ep := range eps.Names() { + if ep == serverEPName { + return + } + } + } + select { + case <-timeout: + f.Fatalf("Timed out waiting for %v to appear in mounttable under %v; found: %v (error: %v)", serverEPName, name, eps, err) + default: + } + time.Sleep(10 * time.Millisecond) + } +} diff --git a/x/ref/services/device/deviced/internal/impl/utiltest/mock_repo.go b/x/ref/services/device/deviced/internal/impl/utiltest/mock_repo.go new file mode 100644 index 000000000..6cbc87903 --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/utiltest/mock_repo.go @@ -0,0 +1,191 @@ +// Copyright 2015 The Vanadium 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 utiltest + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "os" + "reflect" + "testing" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/services/binary" + "v.io/v23/services/repository" + "v.io/v23/verror" +) + +const MockBinaryRepoName = "br" +const MockApplicationRepoName = "ar" + +func StartMockRepos(t *testing.T, ctx *context.T) (*application.Envelope, func()) { + envelope, appCleanup := StartApplicationRepository(ctx) + binaryCleanup := StartBinaryRepository(ctx) + + return envelope, func() { + binaryCleanup() + appCleanup() + } +} + +// StartApplicationRepository sets up a server running the application +// repository. It returns a pointer to the envelope that the repository returns +// to clients (so that it can be changed). It also returns a cleanup function. +func StartApplicationRepository(ctx *context.T) (*application.Envelope, func()) { + invoker := new(arInvoker) + name := MockApplicationRepoName + ctx, cancel := context.WithCancel(ctx) + ctx, server, err := v23.WithNewServer(ctx, name, repository.ApplicationServer(invoker), security.AllowEveryone()) + if err != nil { + ctx.Fatalf("NewServer(%v) failed: %v", name, err) + } + WaitForMount(ctx, ctx, name, server) + return &invoker.envelope, func() { + cancel() + <-server.Closed() + } +} + +// arInvoker holds the state of an application repository invocation mock. The +// mock returns the value of the wrapped envelope, which can be subsequently be +// changed at any time. Client is responsible for synchronization if desired. +type arInvoker struct { + envelope application.Envelope +} + +// APPLICATION REPOSITORY INTERFACE IMPLEMENTATION +func (i *arInvoker) Match(ctx *context.T, _ rpc.ServerCall, profiles []string) (application.Envelope, error) { + ctx.VI(1).Infof("Match()") + if want := []string{"test-profile"}; !reflect.DeepEqual(profiles, want) { + return application.Envelope{}, fmt.Errorf("Expected profiles %v, got %v", want, profiles) + } + return i.envelope, nil +} + +func (i *arInvoker) GetPermissions(ctx *context.T, _ rpc.ServerCall) (perms access.Permissions, version string, err error) { + return nil, "", nil +} + +func (i *arInvoker) SetPermissions(ctx *context.T, _ rpc.ServerCall, perms access.Permissions, version string) error { + return nil +} + +func (i *arInvoker) TidyNow(_ *context.T, _ rpc.ServerCall) error { + return nil +} + +// brInvoker holds the state of a binary repository invocation mock. It always +// serves the current running binary. +type brInvoker struct{} + +// StartBinaryRepository sets up a server running the binary repository and +// returns a cleanup function. +func StartBinaryRepository(ctx *context.T) func() { + name := MockBinaryRepoName + ctx, cancel := context.WithCancel(ctx) + ctx, server, err := v23.WithNewServer(ctx, name, repository.BinaryServer(new(brInvoker)), security.AllowEveryone()) + if err != nil { + ctx.Fatalf("Serve(%q) failed: %v", name, err) + } + WaitForMount(ctx, ctx, name, server) + return func() { + cancel() + <-server.Closed() + } +} + +// BINARY REPOSITORY INTERFACE IMPLEMENTATION + +// TODO(toddw): Move the errors from dispatcher.go into a common location. +const pkgPath = "v.io/x/ref/services/device/deviced/internal/impl/utiltest" + +var ErrOperationFailed = verror.Register(pkgPath+".OperationFailed", verror.NoRetry, "") + +func (*brInvoker) Create(ctx *context.T, _ rpc.ServerCall, _ int32, _ repository.MediaInfo) error { + ctx.VI(1).Infof("Create()") + return nil +} + +func (i *brInvoker) Delete(ctx *context.T, _ rpc.ServerCall) error { + ctx.VI(1).Infof("Delete()") + return nil +} + +func mockBinaryBytesReader() (io.Reader, func(), error) { + file, err := os.Open(os.Args[0]) + if err != nil { + return nil, nil, err + } + cleanup := func() { + file.Close() + } + return file, cleanup, nil +} + +func (i *brInvoker) Download(ctx *context.T, call repository.BinaryDownloadServerCall, _ int32) error { + ctx.VI(1).Infof("Download()") + file, cleanup, err := mockBinaryBytesReader() + if err != nil { + ctx.Errorf("Open() failed: %v", err) + return verror.New(ErrOperationFailed, ctx) + } + defer cleanup() + bufferLength := 4096 + buffer := make([]byte, bufferLength) + sender := call.SendStream() + for { + n, err := file.Read(buffer) + switch err { + case io.EOF: + return nil + case nil: + if err := sender.Send(buffer[:n]); err != nil { + ctx.Errorf("Send() failed: %v", err) + return verror.New(ErrOperationFailed, ctx) + } + default: + ctx.Errorf("Read() failed: %v", err) + return verror.New(ErrOperationFailed, ctx) + } + } +} + +func (*brInvoker) DownloadUrl(ctx *context.T, _ rpc.ServerCall) (string, int64, error) { + ctx.VI(1).Infof("DownloadUrl()") + return "", 0, nil +} + +func (*brInvoker) Stat(ctx *context.T, call rpc.ServerCall) ([]binary.PartInfo, repository.MediaInfo, error) { + ctx.VI(1).Infof("Stat()") + h := md5.New() + bytes, err := ioutil.ReadFile(os.Args[0]) + if err != nil { + return []binary.PartInfo{}, repository.MediaInfo{}, verror.New(ErrOperationFailed, ctx) + } + h.Write(bytes) + part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))} + return []binary.PartInfo{part}, repository.MediaInfo{Type: "application/octet-stream"}, nil +} + +func (i *brInvoker) Upload(ctx *context.T, _ repository.BinaryUploadServerCall, _ int32) error { + ctx.VI(1).Infof("Upload()") + return nil +} + +func (i *brInvoker) GetPermissions(*context.T, rpc.ServerCall) (perms access.Permissions, version string, err error) { + return nil, "", nil +} + +func (i *brInvoker) SetPermissions(_ *context.T, _ rpc.ServerCall, perms access.Permissions, version string) error { + return nil +} diff --git a/x/ref/services/device/deviced/internal/impl/utiltest/modules.go b/x/ref/services/device/deviced/internal/impl/utiltest/modules.go new file mode 100644 index 000000000..130cad45f --- /dev/null +++ b/x/ref/services/device/deviced/internal/impl/utiltest/modules.go @@ -0,0 +1,177 @@ +// Copyright 2015 The Vanadium 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 utiltest + +import ( + "fmt" + "io" + "io/ioutil" + "os" + goexec "os/exec" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/rpc" + "v.io/x/lib/gosh" + "v.io/x/ref" + "v.io/x/ref/internal/logger" + "v.io/x/ref/lib/signals" + "v.io/x/ref/services/device/deviced/internal/starter" + "v.io/x/ref/services/device/deviced/internal/versioning" + "v.io/x/ref/services/device/internal/config" + "v.io/x/ref/services/device/internal/suid" + "v.io/x/ref/test" + "v.io/x/ref/test/v23test" +) + +const ( + RedirectEnv = "DEVICE_MANAGER_DONT_REDIRECT_STDOUT_STDERR" + TestEnvVarName = "V23_RANDOM_ENV_VALUE" + NoPairingToken = "" +) + +// ExecScript launches the script passed as argument. +var ExecScript = gosh.RegisterFunc("ExecScript", func(script string) error { + osenv := []string{RedirectEnv + "=1"} + if os.Getenv("PAUSE_BEFORE_STOP") == "1" { + osenv = append(osenv, "PAUSE_BEFORE_STOP=1") + } + cmd := goexec.Cmd{ + Path: script, + Env: osenv, + Stdin: os.Stdin, + Stderr: os.Stderr, + Stdout: os.Stdout, + } + return cmd.Run() +}) + +// DeviceManager sets up a device manager server. It accepts the name to +// publish the server under as an argument. Additional arguments can optionally +// specify device manager config settings. +var DeviceManager = gosh.RegisterFunc("DeviceManager", deviceManagerFunc) + +func waitForEOF(r io.Reader) { + io.Copy(ioutil.Discard, r) +} + +func deviceManagerFunc(publishName string, args ...string) error { + ctx, shutdown := test.V23Init() + defer shutdown() + + defer fmt.Printf("%v terminated\n", publishName) + defer ctx.VI(1).Infof("%v terminated", publishName) + + // Satisfy the contract described in doc.go by passing the config state + // through to the device manager dispatcher constructor. + configState, err := config.Load() + if err != nil { + ctx.Fatalf("Failed to decode config state: %v", err) + } + + // This exemplifies how to override or set specific config fields, if, + // for example, the device manager is invoked 'by hand' instead of via a + // script prepared by a previous version of the device manager. + var pairingToken string + if len(args) > 0 { + if want, got := 4, len(args); want > got { + ctx.Fatalf("expected atleast %d additional arguments, got %d instead: %q", want, got, args) + } + configState.Root, configState.Helper, configState.Origin, configState.CurrentLink = args[0], args[1], args[2], args[3] + if len(args) > 4 { + pairingToken = args[4] + } + } + // We grab the shutdown channel at this point in order to ensure that we + // register a listener for the app cycle manager Stop before we start + // running the device manager service. Otherwise, any device manager + // method that calls Stop on the app cycle manager (e.g. the Stop RPC) + // will precipitate an immediate process exit. + shutdownChan := signals.ShutdownOnSignals(ctx) + listenSpec := rpc.ListenSpec{Addrs: rpc.ListenAddrs{{"tcp", "127.0.0.1:0"}}} + blessings, _ := v23.GetPrincipal(ctx).BlessingStore().Default() + claimableEps, stop, err := starter.Start(ctx, starter.Args{ + Namespace: starter.NamespaceArgs{ + ListenSpec: listenSpec, + }, + Device: starter.DeviceArgs{ + Name: publishName, + ListenSpec: listenSpec, + ConfigState: configState, + TestMode: strings.HasSuffix(fmt.Sprint(blessings), ":testdm"), + RestartCallback: func() { fmt.Println("restart handler") }, + PairingToken: pairingToken, + }, + // TODO(rthellend): Wire up the local mounttable like the real device + // manager, i.e. mount the device manager and the apps on it, and mount + // the local mounttable in the global namespace. + // MountGlobalNamespaceInLocalNamespace: true, + }) + if err != nil { + ctx.Errorf("starter.Start failed: %v", err) + return err + } + defer stop() + // Update the namespace roots to remove the server blessing from the + // endpoints. This is needed to be able to publish into the 'global' + // mounttable before we have compatible credentials. + ctx, err = SetNamespaceRootsForUnclaimedDevice(ctx) + if err != nil { + return err + } + // Manually mount the claimable service in the 'global' mounttable. + for _, ep := range claimableEps { + v23.GetNamespace(ctx).Mount(ctx, "claimable", ep.Name(), 0) + } + fmt.Println("READY") + + <-shutdownChan + if os.Getenv("PAUSE_BEFORE_STOP") == "1" { + waitForEOF(os.Stdin) + } + // TODO(ashankar): Figure out a way to incorporate this check in the test. + // if impl.DispatcherLeaking(dispatcher) { + // ctx.Fatalf("device manager leaking resources") + // } + return nil +} + +// This is the same as DeviceManager above, except that it has a different major +// version number. +var DeviceManagerV10 = gosh.RegisterFunc("DeviceManagerV10", func(publishName string, args ...string) error { + versioning.CurrentVersion = versioning.Version{10, 0} // Set the version number to 10.0 + return deviceManagerFunc(publishName, args...) +}) + +func DeviceManagerCmd(sh *v23test.Shell, f *gosh.Func, args ...interface{}) *v23test.Cmd { + dm := sh.FuncCmd(f, args...) + // Make sure the device manager command is not provided with credentials. + delete(dm.Vars, ref.EnvCredentials) + delete(dm.Vars, ref.EnvAgentPath) + return dm +} + +func TestMainImpl(m *testing.M) { + isSuidHelper := len(os.Getenv("V23_SUIDHELPER_TEST")) > 0 + if isSuidHelper { + os.Exit(m.Run()) + } + v23test.TestMain(m) +} + +// TestSuidHelper is testing boilerplate for suidhelper that does not +// create a runtime because the suidhelper is not a Vanadium application. +func TestSuidHelperImpl(t *testing.T) { + if os.Getenv("V23_SUIDHELPER_TEST") != "1" { + return + } + logger.Global().VI(1).Infof("TestSuidHelper starting") + if err := suid.Run(os.Environ()); err != nil { + logger.Global().Fatalf("Failed to Run() setuidhelper: %v", err) + } + // Don't show "PASS" + os.Exit(0) +} diff --git a/x/ref/services/device/deviced/internal/installer/device_installer.go b/x/ref/services/device/deviced/internal/installer/device_installer.go new file mode 100644 index 000000000..8c54d2a06 --- /dev/null +++ b/x/ref/services/device/deviced/internal/installer/device_installer.go @@ -0,0 +1,415 @@ +// Copyright 2015 The Vanadium 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 installer contains logic responsible for managing the device manager +// server, including setting it up / tearing it down, and starting / stopping +// it. +package installer + +// When setting up the device manager installation, the installer creates a +// directory structure from which the device manager can be run. It sets up: +// +// <installDir> - provided when installer is invoked +// dmroot/ - created/owned by the installation +// device-manager/ - will be the root for the device manager server; +// set as <Config.Root> (see comment in +// device_service.go for what goes under here) +// info - json-encoded info about the running device manager (currently just the pid) +// base/ - initial installation of device manager +// deviced - link to deviced (self) +// deviced.sh - script to start the device manager +// device-data/ +// persistent-args - list of persistent arguments for the device +// manager (json encoded) +// logs/ - device manager logs will go here +// current - set as <Config.CurrentLink> +// creation_info - json-encoded info about the binary that created the directory tree +// deviced.sh - script to launch device manager under restarter +// security/ - security agent keeps credentials here +// principal/ +// dm_logs/ - restarter logs +// STDERR-<timestamp> +// STDOUT-<timestamp> +// service_description - json-encoded sysinit device manager config +// inithelper - soft link to init helper +// +// TODO: we should probably standardize on '-' vs '_' for multi-word filename separators. Note any change +// in the name of creation_info will require some care to ensure the version check continues to work. + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "os/user" + "path/filepath" + "syscall" + "time" + + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/services/application" + "v.io/x/lib/envvar" + "v.io/x/ref" + "v.io/x/ref/lib/security" + "v.io/x/ref/services/device/deviced/internal/impl" + "v.io/x/ref/services/device/deviced/internal/versioning" + "v.io/x/ref/services/device/internal/config" + "v.io/x/ref/services/device/internal/sysinit" +) + +// restartExitCode is the exit code that the device manager should return when +// it wants to be restarted by its parent (i.e., the restarter). This number is +// picked quasi-arbitrarily from the set of exit codes without prior special +// meanings. +const restartExitCode = 140 + +// dmRoot is the directory name where the device manager installs itself. +const dmRoot = "dmroot" + +// InstallFrom takes a vanadium object name denoting an application service +// where a device manager application envelope can be obtained. It downloads +// the latest version of the device manager and installs it. +func InstallFrom(origin string) error { + // TODO(caprita): Implement. + return nil +} + +// initCommand verifies if init mode is enabled, and if so executes the +// appropriate sysinit command. Returns whether init mode was detected, as well +// as any error encountered. +func initCommand(root, command string, stderr, stdout io.Writer) (bool, error) { + sdFile := filepath.Join(root, "service_description") + if _, err := os.Stat(sdFile); err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, fmt.Errorf("Stat(%v) failed: %v", sdFile, err) + } + helperLink := filepath.Join(root, "inithelper") + cmd := exec.Command(helperLink, fmt.Sprintf("--service_description=%s", sdFile), command) + if stderr != nil { + cmd.Stderr = stderr + } + if stdout != nil { + cmd.Stdout = stdout + } + if err := cmd.Run(); err != nil { + return true, fmt.Errorf("Running init helper %v failed: %v", command, err) + } + return true, nil +} + +func addToPATH(env []string, dir string) []string { + e := envvar.VarsFromSlice(env) + if !e.Contains("PATH") { + e.Set("PATH", dir) + } else { + e.Set("PATH", dir+":"+e.Get("PATH")) + } + return e.ToSlice() +} + +// SelfInstall installs the device manager and configures it using the +// environment and the supplied command-line flags. +func SelfInstall(ctx *context.T, installDir, suidHelper, restarter, agent, initHelper, origin string, singleUser, sessionMode, init bool, args, env []string, stderr, stdout io.Writer) error { + if os.Getenv(ref.EnvCredentials) != "" { + return fmt.Errorf("Attempting to install device manager with the %q environment variable set.", ref.EnvCredentials) + } + root := filepath.Join(installDir, dmRoot) + if _, err := os.Stat(root); err == nil || !os.IsNotExist(err) { + return fmt.Errorf("%v already exists", root) + } + deviceDir := filepath.Join(root, "device-manager", "base") + perm := os.FileMode(0711) + if err := os.MkdirAll(deviceDir, perm); err != nil { + return fmt.Errorf("MkdirAll(%v, %v) failed: %v", deviceDir, perm, err) + } + + // save info about the binary creating this tree + if err := versioning.SaveCreatorInfo(ctx, root); err != nil { + return err + } + + currLink := filepath.Join(root, "current") + configState := &config.State{ + Name: "dummy", // So that Validate passes. + Root: root, + Origin: origin, + CurrentLink: currLink, + Helper: suidHelper, + } + if err := configState.Validate(); err != nil { + return fmt.Errorf("invalid config %v: %v", configState, err) + } + var extraArgs []string + if name, err := os.Hostname(); err == nil { + extraArgs = append(extraArgs, fmt.Sprintf("--name=%q", naming.Join("devices", name))) + } + if !sessionMode { + extraArgs = append(extraArgs, fmt.Sprintf("--restart-exit-code=%d", restartExitCode)) + } + if agent != "" { + if agentBinName := filepath.Base(agent); agentBinName != "v23agentd" { + return fmt.Errorf("agent must be called v23agentd; got %v instead", agentBinName) + } + // Make the agent available in the PATH when the device manager + // loads the credentials from disk. + env = addToPATH(env, filepath.Dir(agent)) + } + envelope := &application.Envelope{ + Args: append(extraArgs, args...), + // TODO(caprita): Cleaning up env vars to avoid picking up all + // the garbage from the user's env. + // Alternatively, pass the env vars meant specifically for the + // device manager in a different way. + Env: impl.VanadiumEnvironment(env), + } + if err := impl.SavePersistentArgs(root, envelope.Args); err != nil { + return err + } + if err := impl.LinkSelf(deviceDir, "deviced"); err != nil { + return err + } + configSettings, err := configState.Save(nil) + if err != nil { + return fmt.Errorf("failed to serialize config %v: %v", configState, err) + } + logs := filepath.Join(root, "device-manager", "logs") + if err := impl.GenerateScript(deviceDir, configSettings, envelope, logs); err != nil { + return err + } + + // TODO(caprita): Test the device manager we just installed. + if err := impl.UpdateLink(filepath.Join(deviceDir, "deviced.sh"), currLink); err != nil { + return err + } + + if err := generateDMScript(root, restarter, agent, currLink, singleUser, sessionMode); err != nil { + return err + } + if init { + dmScript := filepath.Join(root, "deviced.sh") + currentUser, err := user.Current() + if err != nil { + return err + } + sd := &sysinit.ServiceDescription{ + Service: "deviced", + Description: "Vanadium Device Manager", + Binary: dmScript, + Command: []string{dmScript}, + User: currentUser.Username, + } + sdFile := filepath.Join(root, "service_description") + if err := sd.SaveTo(sdFile); err != nil { + return fmt.Errorf("SaveTo for %v failed: %v", sd, err) + } + helperLink := filepath.Join(root, "inithelper") + if err := os.Symlink(initHelper, helperLink); err != nil { + return fmt.Errorf("Symlink(%v, %v) failed: %v", initHelper, helperLink, err) + } + if initMode, err := initCommand(root, "install", stderr, stdout); err != nil { + return err + } else if !initMode { + return fmt.Errorf("enabling init mode failed") + } + } + return nil +} + +func generateDMScript(workspace, restarter, agent, currLink string, singleUser, sessionMode bool) error { + securityDir := filepath.Join(workspace, "security") + principalDir := filepath.Join(securityDir, "principal") + perm := os.FileMode(0700) + if _, err := security.CreatePersistentPrincipal(principalDir, nil); err != nil { + return fmt.Errorf("CreatePersistentPrincipal(%v, nil) failed: %v", principalDir, err) + } + logs := filepath.Join(workspace, "dm_logs") + if err := os.MkdirAll(logs, perm); err != nil { + return fmt.Errorf("MkdirAll(%v, %v) failed: %v", logs, perm, err) + } + stdoutLog, stderrLog := filepath.Join(logs, "STDOUT"), filepath.Join(logs, "STDERR") + // TODO(caprita): Switch all our generated bash scripts to use templates. + output := "#!" + impl.ShellPath + "\n" + output += "if [ -z \"$DEVICE_MANAGER_DONT_REDIRECT_STDOUT_STDERR\" ]; then\n" + output += fmt.Sprintf(" TIMESTAMP=$(%s)\n", impl.DateCommand) + output += fmt.Sprintf(" exec > %s-$TIMESTAMP 2> %s-$TIMESTAMP\n", stdoutLog, stderrLog) + output += "fi\n" + output += fmt.Sprintf("%s=%q ", ref.EnvCredentials, principalDir) + // 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 + // v.io/x/ref/services/debug/debug/impl.go) instead. + output += fmt.Sprintf("exec %q ", restarter) + if !sessionMode { + output += fmt.Sprintf("--restart-exit-code=!0 ") + } + output += fmt.Sprintf("%q", currLink) + path := filepath.Join(workspace, "deviced.sh") + if err := ioutil.WriteFile(path, []byte(output), 0700); err != nil { + return fmt.Errorf("WriteFile(%v) failed: %v", path, err) + } + // TODO(caprita): Put logs under dmroot/device-manager/logs. + return nil +} + +// Uninstall undoes SelfInstall, removing the device manager's installation +// directory. +func Uninstall(ctx *context.T, installDir, helperPath string, stdout, stderr io.Writer) error { + // TODO(caprita): ensure device is stopped? + + root := filepath.Join(installDir, dmRoot) + if _, err := initCommand(root, "uninstall", stdout, stderr); err != nil { + return err + } + impl.InitSuidHelper(ctx, helperPath) + return impl.DeleteFileTree(ctx, root, stdout, stderr) +} + +// Start starts the device manager. +func Start(ctx *context.T, installDir string, stderr, stdout io.Writer) error { + // TODO(caprita): make sure it's not already running? + + root := filepath.Join(installDir, dmRoot) + + if initMode, err := initCommand(root, "start", stderr, stdout); err != nil { + return err + } else if initMode { + return nil + } + + if os.Getenv(ref.EnvCredentials) != "" { + return fmt.Errorf("Attempting to run device manager with the %q environment variable set.", ref.EnvCredentials) + } + dmScript := filepath.Join(root, "deviced.sh") + cmd := exec.Command(dmScript) + if stderr != nil { + cmd.Stderr = stderr + } + if stdout != nil { + cmd.Stdout = stdout + } + if err := cmd.Start(); err != nil { + return fmt.Errorf("Start failed: %v", err) + } + + // Save away the restarter's pid to be used for stopping later ... + if cmd.Process.Pid == 0 { + fmt.Fprintf(stderr, "Unable to get a pid for successfully-started restarterr!") + return nil // We tolerate the error, at the expense of being able to stop later + } + mi := &impl.ManagerInfo{ + Pid: cmd.Process.Pid, + } + if err := impl.SaveManagerInfo(filepath.Join(root, "restarter-deviced"), mi); err != nil { + return fmt.Errorf("failed to save info for restarter-deviced: %v", err) + } + + return nil +} + +// Stop stops the device manager. +func Stop(ctx *context.T, installDir string, stderr, stdout io.Writer) error { + root := filepath.Join(installDir, dmRoot) + if initMode, err := initCommand(root, "stop", stderr, stdout); err != nil { + return err + } else if initMode { + return nil + } + + restarterPid, devmgrPid := 0, 0 + + // Load the restarter pid + info, err := impl.LoadManagerInfo(filepath.Join(root, "restarter-deviced")) + if err != nil { + return fmt.Errorf("loadManagerInfo failed for restarter-deviced: %v", err) + } + if syscall.Kill(info.Pid, 0) == nil { // Save the pid if it's currently live + restarterPid = info.Pid + } + + // Load the device manager pid + info, err = impl.LoadManagerInfo(filepath.Join(root, "device-manager")) + if err != nil { + return fmt.Errorf("loadManagerInfo failed for device-manager: %v", err) + } + if syscall.Kill(info.Pid, 0) == nil { // Save the pid if it's currently live + devmgrPid = info.Pid + } + + if restarterPid == 0 && devmgrPid == 0 { + return fmt.Errorf("stop could not find any live pids to stop") + } + + // Set up waiters for each nonzero pid. This ensures that exiting + // processes are reaped when the restarter or device manager happen to + // be children of this process. (Not commonly the case, but it does + // occur in the impl test.) + if restarterPid != 0 { + go func() { + if p, err := os.FindProcess(restarterPid); err == nil { + p.Wait() + } + }() + } + if devmgrPid != 0 { + go func() { + if p, err := os.FindProcess(devmgrPid); err == nil { + p.Wait() + } + }() + } + + // First, send SIGINT to the restarter. We expect both the restarter and the device manager to + // exit as a result within 15 seconds + if restarterPid != 0 { + if err = syscall.Kill(restarterPid, syscall.SIGINT); err != nil { + return fmt.Errorf("sending SIGINT to %d: %v", restarterPid, err) + } + for i := 0; i < 30 && syscall.Kill(restarterPid, 0) == nil; i++ { + time.Sleep(500 * time.Millisecond) + if i%5 == 4 { + fmt.Fprintf(stderr, "waiting for restarter (pid %d) to die...\n", restarterPid) + } + } + if syscall.Kill(restarterPid, 0) == nil { // restarter is still alive, resort to brute force + fmt.Fprintf(stderr, "sending SIGKILL to restarter %d\n", restarterPid) + if err = syscall.Kill(restarterPid, syscall.SIGKILL); err != nil { + fmt.Fprintf(stderr, "Sending SIGKILL to %d: %v\n", restarterPid, err) + // not returning here, so that we check & kill the device manager too + } + } + } + + // If the device manager is still alive, forcibly kill it + if syscall.Kill(devmgrPid, 0) == nil { + fmt.Fprintf(stderr, "sending SIGKILL to device manager %d\n", devmgrPid) + if err = syscall.Kill(devmgrPid, syscall.SIGKILL); err != nil { + return fmt.Errorf("sending SIGKILL to device manager %d: %v", devmgrPid, err) + } + } + + // By now, nothing should be alive. Check and report + if restarterPid != 0 && syscall.Kill(restarterPid, 0) == nil { + return fmt.Errorf("multiple attempts to kill restarter pid %d have failed", restarterPid) + } + if devmgrPid != 0 && syscall.Kill(devmgrPid, 0) == nil { + return fmt.Errorf("multiple attempts to kill device manager pid %d have failed", devmgrPid) + } + + // Should we remove the restarter and deviced info files here? Not removing them + // increases the chances that we later rerun stop and shoot some random process. Removing + // them makes it impossible to run stop a second time (although that shouldn't be necessary) + // and also introduces the potential for a race condition if a new restarter/deviced are started + // right after these ones get killed. + // + // TODO: Reconsider this when we add stronger protection to make sure that the pids being + // signalled are in fact the restarter and/or device manager + + // Process was killed succesfully + return nil +} diff --git a/x/ref/services/device/deviced/internal/starter/starter.go b/x/ref/services/device/deviced/internal/starter/starter.go new file mode 100644 index 000000000..e269ff0b1 --- /dev/null +++ b/x/ref/services/device/deviced/internal/starter/starter.go @@ -0,0 +1,307 @@ +// Copyright 2015 The Vanadium 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 starter provides a single function that starts up servers for a +// mounttable and a device manager that is mounted on it. +package starter + +import ( + "encoding/base64" + "os" + "path/filepath" + "time" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/options" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/verror" + displib "v.io/x/ref/lib/dispatcher" + _ "v.io/x/ref/runtime/factories/roaming" + "v.io/x/ref/services/debug/debuglib" + "v.io/x/ref/services/device/deviced/internal/impl" + "v.io/x/ref/services/device/deviced/internal/versioning" + "v.io/x/ref/services/device/internal/claim" + "v.io/x/ref/services/device/internal/config" + "v.io/x/ref/services/internal/pathperms" + "v.io/x/ref/services/mounttable/mounttablelib" +) + +const pkgPath = "v.io/x/ref/services/device/deviced/internal/starter" + +var ( + errCantSaveInfo = verror.Register(pkgPath+".errCantSaveInfo", verror.NoRetry, "{1:}{2:} failed to save info{:_}") +) + +type NamespaceArgs struct { + Name string // Name to publish the mounttable service under (after claiming). + ListenSpec rpc.ListenSpec // ListenSpec for the server. + PermissionsFile string // Path to the Permissions file used by the mounttable. + PersistenceDir string // Path to the directory holding persistent acls. + // Name in the local neighborhood on which to make the mounttable + // visible. If empty, the mounttable will not be visible in the local + // neighborhood. + Neighborhood string +} + +type DeviceArgs struct { + Name string // Name to publish the device service under (after claiming). + ListenSpec rpc.ListenSpec // ListenSpec for the device server. + ConfigState *config.State // Configuration for the device. + TestMode bool // Whether the device is running in test mode or not. + RestartCallback func() // Callback invoked when the device service is restarted. + PairingToken string // PairingToken that a claimer needs to provide. +} + +func (d *DeviceArgs) name(mt string) string { + if d.Name != "" { + return d.Name + } + return naming.Join(mt, "devmgr") +} + +type Args struct { + Namespace NamespaceArgs + Device DeviceArgs + + // If true, the global namespace will be made available on the + // mounttable server under "global/". + MountGlobalNamespaceInLocalNamespace bool +} + +// Start creates servers for the mounttable and device services and links them together. +// +// Returns the endpoints for the claimable service (empty if already claimed), +// a callback to be invoked to shutdown the services on success, or an error on +// failure. +func Start(ctx *context.T, args Args) ([]naming.Endpoint, func(), error) { + // Is this binary compatible with the state on disk? + if err := versioning.CheckCompatibility(ctx, args.Device.ConfigState.Root); err != nil { + return nil, nil, err + } + // In test mode, we skip writing the info file to disk, and we skip + // attempting to start the claimable service: the device must have been + // claimed already to enable updates anyway, and checking for perms in + // NewClaimableDispatcher needlessly prints a perms signature + // verification error to the logs. + if args.Device.TestMode { + cleanup, err := startClaimedDevice(ctx, args) + return nil, cleanup, err + } + + // TODO(caprita): use some mechanism (a file lock or presence of entry + // in mounttable) to ensure only one device manager is running in an + // installation? + mi := &impl.ManagerInfo{ + Pid: os.Getpid(), + } + if err := impl.SaveManagerInfo(filepath.Join(args.Device.ConfigState.Root, "device-manager"), mi); err != nil { + return nil, nil, verror.New(errCantSaveInfo, ctx, err) + } + + // If the device has not yet been claimed, start the mounttable and + // claimable service and wait for it to be claimed. + // Once a device is claimed, close any previously running servers and + // start a new mounttable and device service. + claimable, claimed := claim.NewClaimableDispatcher(ctx, impl.PermsDir(args.Device.ConfigState), args.Device.PairingToken, security.AllowEveryone()) + if claimable == nil { + // Device has already been claimed, bypass claimable service + // stage. + cleanup, err := startClaimedDevice(ctx, args) + return nil, cleanup, err + } + eps, stopClaimable, err := startClaimableDevice(ctx, claimable, args) + if err != nil { + return nil, nil, err + } + stop := make(chan struct{}) + stopped := make(chan struct{}) + go waitToBeClaimedAndStartClaimedDevice(ctx, stopClaimable, claimed, stop, stopped, args) + return eps, func() { + close(stop) + <-stopped + }, nil +} + +func startClaimableDevice(ctx *context.T, dispatcher rpc.Dispatcher, args Args) ([]naming.Endpoint, func(), error) { + ctx, cancel := context.WithCancel(ctx) + ctx = v23.WithListenSpec(ctx, args.Device.ListenSpec) + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", dispatcher, options.LameDuckTimeout(30*time.Second)) + if err != nil { + cancel() + return nil, nil, err + } + shutdown := func() { + cancel() + ctx.Infof("Stopping claimable server...") + <-server.Closed() + ctx.Infof("Stopped claimable server.") + } + publicKey, err := v23.GetPrincipal(ctx).PublicKey().MarshalBinary() + if err != nil { + shutdown() + return nil, nil, err + } + var eps []naming.Endpoint + if proxy := args.Device.ListenSpec.Proxy; proxy != "" { + for { + status := server.Status() + if err, ok := status.ProxyErrors[proxy]; ok && err == nil { + eps = status.Endpoints + break + } + ctx.Infof("Waiting for proxy address to appear...") + <-status.Dirty + } + } else { + eps = server.Status().Endpoints + } + ctx.Infof("Unclaimed device manager with public_key: %s", base64.URLEncoding.EncodeToString(publicKey)) + for _, ep := range eps { + ctx.Infof("Unclaimed device manager endpoint: %v", ep.Name()) + } + ctx.FlushLog() + return eps, shutdown, nil +} + +func waitToBeClaimedAndStartClaimedDevice(ctx *context.T, stopClaimable func(), claimed, stop <-chan struct{}, stopped chan<- struct{}, args Args) { + // Wait for either the claimable service to complete, or be stopped + defer close(stopped) + select { + case <-claimed: + stopClaimable() + case <-stop: + stopClaimable() + return + } + shutdown, err := startClaimedDevice(ctx, args) + if err != nil { + ctx.Errorf("Failed to start device service after it was claimed: %v", err) + v23.GetAppCycle(ctx).Stop(ctx) + return + } + defer shutdown() + <-stop // Wait to be stopped +} + +func startClaimedDevice(ctx *context.T, args Args) (func(), error) { + ctx.Infof("Starting claimed device services...") + permStore := pathperms.NewPathStore(ctx) + permsDir := impl.PermsDir(args.Device.ConfigState) + debugAuth, err := pathperms.NewHierarchicalAuthorizer(permsDir, permsDir, permStore) + if err != nil { + return nil, err + } + + debugDisp := debuglib.NewDispatcher(debugAuth) + + ctx = v23.WithReservedNameDispatcher(ctx, debugDisp) + + ctx.Infof("Starting mount table...") + mtName, stopMT, err := startMounttable(ctx, args.Namespace) + if err != nil { + ctx.Errorf("Failed to start mounttable service: %v", err) + return nil, err + } else { + ctx.Infof("Started mount table.") + } + ctx.Infof("Starting device service...") + stopDevice, err := startDeviceServer(ctx, args.Device, mtName, permStore) + if err != nil { + ctx.Errorf("Failed to start device service: %v", err) + stopMT() + return nil, err + } else { + ctx.Infof("Started device service.") + } + if args.MountGlobalNamespaceInLocalNamespace { + ctx.Infof("Mounting %v ...", mtName) + mountGlobalNamespaceInLocalNamespace(ctx, mtName) + ctx.Infof("Mounted %v", mtName) + } + + impl.InvokeCallback(ctx, args.Device.ConfigState.Name) + + ctx.Infof("Started claimed device services.") + return func() { + stopDevice() + stopMT() + }, nil +} + +func startMounttable(ctx *context.T, n NamespaceArgs) (string, func(), error) { + mtName, stopMT, err := mounttablelib.StartServers(ctx, n.ListenSpec, n.Name, n.Neighborhood, n.PermissionsFile, n.PersistenceDir, "mounttable") + if err != nil { + ctx.Errorf("mounttablelib.StartServers(%#v) failed: %v", n, err) + } else { + ctx.Infof("Local mounttable (%v) published as %q", mtName, n.Name) + } + return mtName, func() { + ctx.Infof("Stopping mounttable...") + stopMT() + ctx.Infof("Stopped mounttable.") + }, err +} + +// startDeviceServer creates an rpc.Server and sets it up to server the Device service. +// +// ls: ListenSpec for the server +// configState: configuration for the Device service dispatcher +// mt: Object address of the mounttable +// dm: Name to publish the device service under +// testMode: whether the service is to be run in test mode +// restarted: callback invoked when the device manager is restarted. +// +// Returns: +// (1) Function to be called to force the service to shutdown +// (2) Any errors in starting the service (in which case, (1) will be nil) +func startDeviceServer(ctx *context.T, args DeviceArgs, mt string, permStore *pathperms.PathStore) (shutdown func(), err error) { + ctx = v23.WithListenSpec(ctx, args.ListenSpec) + wrapper := displib.NewDispatcherWrapper() + ctx, cancel := context.WithCancel(ctx) + ctx, server, err := v23.WithNewDispatchingServer(ctx, args.name(mt), wrapper) + if err != nil { + cancel() + return nil, err + } + args.ConfigState.Name = server.Status().Endpoints[0].Name() + + dispatcher, dShutdown, err := impl.NewDispatcher(ctx, args.ConfigState, mt, args.TestMode, args.RestartCallback, permStore) + if err != nil { + cancel() + <-server.Closed() + return nil, err + } + + shutdown = func() { + // TODO(caprita): Capture the Dying state by feeding it back to + // the dispatcher and exposing it in Status. + ctx.Infof("Stopping device server...") + cancel() + <-server.Closed() + dShutdown() + ctx.Infof("Stopped device.") + } + wrapper.SetDispatcher(dispatcher) + ctx.Infof("Device manager (%v) published as %v", args.ConfigState.Name, args.name(mt)) + return shutdown, nil +} + +func mountGlobalNamespaceInLocalNamespace(ctx *context.T, localMT string) { + ns := v23.GetNamespace(ctx) + for _, root := range ns.Roots() { + go func(r string) { + for { + err := ns.Mount(ctx, naming.Join(localMT, "global"), r, 0 /* forever */, naming.ServesMountTable(true)) + if err == nil { + break + } + ctx.Infof("Failed to Mount global namespace: %v", err) + time.Sleep(time.Second) + } + }(root) + } +} diff --git a/x/ref/services/device/deviced/internal/versioning/creator_info.go b/x/ref/services/device/deviced/internal/versioning/creator_info.go new file mode 100644 index 000000000..117de75e7 --- /dev/null +++ b/x/ref/services/device/deviced/internal/versioning/creator_info.go @@ -0,0 +1,85 @@ +// Copyright 2015 The Vanadium 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 versioning handles device manager versioning. Device manager +// binaries need to be compatible with the existing device manager installation. +package versioning + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + + "v.io/v23/context" + "v.io/v23/verror" + "v.io/x/lib/metadata" + "v.io/x/ref/services/device/internal/errors" +) + +// Version info for this device manager binary. Increment as appropriate when the binary changes. +// The major version number should be incremented whenever a change to the binary makes it incompatible +// with on-disk state created by a binary from a different major version. +type Version struct{ Major, Minor int } + +var CurrentVersion = Version{1, 0} + +// CreationInfo holds data about the binary that originally created the device manager on-disk state +type CreatorInfo struct { + Version Version + MetaData string +} + +func SaveCreatorInfo(ctx *context.T, dir string) error { + info := CreatorInfo{ + Version: CurrentVersion, + MetaData: metadata.ToXML(), + } + jsonInfo, err := json.Marshal(info) + if err != nil { + ctx.Errorf("Marshal(%v) failed: %v", info, err) + return verror.New(errors.ErrOperationFailed, nil) + } + if err := os.MkdirAll(dir, os.FileMode(0700)); err != nil { + ctx.Errorf("MkdirAll(%v) failed: %v", dir, err) + return verror.New(errors.ErrOperationFailed, nil) + } + infoPath := filepath.Join(dir, "creation_info") + if err := ioutil.WriteFile(infoPath, jsonInfo, 0600); err != nil { + ctx.Errorf("WriteFile(%v) failed: %v", infoPath, err) + return verror.New(errors.ErrOperationFailed, nil) + } + // Make the file read-only as we don't want anyone changing it + if err := os.Chmod(infoPath, 0400); err != nil { + ctx.Errorf("Chmod(0400, %v) failed: %v", infoPath, err) + return verror.New(errors.ErrOperationFailed, nil) + } + return nil +} + +func loadCreatorInfo(ctx *context.T, dir string) (*CreatorInfo, error) { + infoPath := filepath.Join(dir, "creation_info") + info := new(CreatorInfo) + if infoBytes, err := ioutil.ReadFile(infoPath); err != nil { + ctx.Errorf("ReadFile(%v) failed: %v", infoPath, err) + return nil, verror.New(errors.ErrOperationFailed, nil) + } else if err := json.Unmarshal(infoBytes, info); err != nil { + ctx.Errorf("Unmarshal(%v) failed: %v", infoBytes, err) + return nil, verror.New(errors.ErrOperationFailed, nil) + } + return info, nil +} + +// Checks the compatibilty of the running binary against the device manager directory on disk +func CheckCompatibility(ctx *context.T, dir string) error { + if infoOnDisk, err := loadCreatorInfo(ctx, dir); err != nil { + ctx.Errorf("Failed to load creator info from %s", dir) + return verror.New(errors.ErrOperationFailed, nil) + } else if CurrentVersion.Major != infoOnDisk.Version.Major { + ctx.Errorf("Device Manager binary vs disk major version mismatch (%+v vs %+v)", + CurrentVersion, infoOnDisk.Version) + return verror.New(errors.ErrOperationFailed, nil) + } + return nil +} diff --git a/x/ref/services/device/deviced/main.go b/x/ref/services/device/deviced/main.go new file mode 100644 index 000000000..1bb434be0 --- /dev/null +++ b/x/ref/services/device/deviced/main.go @@ -0,0 +1,42 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The following enables go generate to generate the doc.go file. +//go:generate gendoc . + +package main + +import ( + "fmt" + "os" + "runtime" + "syscall" + + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" +) + +func main() { + // TODO(caprita): Remove this once we have a way to set the GOMAXPROCS + // environment variable persistently for device manager. + if os.Getenv("GOMAXPROCS") == "" { + runtime.GOMAXPROCS(runtime.NumCPU()) + } + // Make deviced the leader of a new process group. + if err := syscall.Setpgid(0, 0); err != nil { + fmt.Fprintf(os.Stderr, "Setpgid failed: %v\n", err) + } + + rootCmd := &cmdline.Command{ + Name: "deviced", + Short: "launch, configure and manage the deviced daemon", + Long: ` +Command deviced is used to launch, configure and manage the deviced daemon, +which implements the v.io/v23/services/device interfaces. +`, + Children: []*cmdline.Command{cmdInstall, cmdUninstall, cmdStart, cmdStop, cmdProfile}, + Runner: v23cmd.RunnerFunc(runServer), + } + cmdline.Main(rootCmd) +} diff --git a/x/ref/services/device/deviced/server.go b/x/ref/services/device/deviced/server.go new file mode 100644 index 000000000..2af54a370 --- /dev/null +++ b/x/ref/services/device/deviced/server.go @@ -0,0 +1,148 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "crypto/rand" + "encoding/base64" + "flag" + "net" + "os" + "path/filepath" + "regexp" + "strconv" + "time" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/rpc" + "v.io/v23/verror" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/exec" + "v.io/x/ref/lib/mgmt" + "v.io/x/ref/lib/signals" + _ "v.io/x/ref/runtime/factories/roaming" + "v.io/x/ref/services/device/deviced/internal/starter" + "v.io/x/ref/services/device/internal/config" +) + +const pkgPath = "v.io/x/ref/services/device/deviced" + +var ( + errSplitHostPortFailed = verror.Register(pkgPath+".errSplitHostPortFailed", verror.NoRetry, "{1:}{2:} net.SplitHostPort({3}) failed{:_}") +) + +var ( + // TODO(caprita): publishAs and restartExitCode should be provided by the + // config? + publishAs = flag.String("name", "", "name to publish the device manager at") + restartExitCode = flag.Int("restart-exit-code", 0, "exit code to return when device manager should be restarted") + nhName = flag.String("neighborhood-name", "", `if provided, it will enable sharing with the local neighborhood with the provided name. The address of the local mounttable will be published to the neighboorhood and everything in the neighborhood will be visible on the local mounttable.`) + dmPort = flag.Int("deviced-port", 0, "the port number of assign to the device manager service. The hostname/IP address part of --v23.tcp.address is used along with this port. By default, the port is assigned by the OS.") + usePairingToken = flag.Bool("use-pairing-token", false, "generate a pairing token for the device manager that will need to be provided when a device is claimed") +) + +func init() { + cmdline.HideGlobalFlagsExcept(regexp.MustCompile(`^((name)|(restart-exit-code)|(neighborhood-name)|(deviced-port)|(use-pairing-token))$`)) +} + +func runServer(ctx *context.T, _ *cmdline.Env, _ []string) error { + var testMode bool + + // If this device manager was started by another device manager, it must + // be part of a self update to test that this binary works. In that + // case, we need to disable a lot of functionality. + if parentConfig, err := exec.ReadConfigFromOSEnv(); parentConfig != nil && err == nil { + if _, err := parentConfig.Get(mgmt.ParentNameConfigKey); err == nil { + testMode = true + ctx.Infof("TEST MODE") + } + } + + configState, err := config.Load() + if err != nil { + ctx.Errorf("Failed to load config passed from parent: %v", err) + return err + } + mtPermsDir := filepath.Join(configState.Root, "mounttable") + if err := os.MkdirAll(mtPermsDir, 0700); err != nil { + ctx.Errorf("os.MkdirAll(%q) failed: %v", mtPermsDir, err) + return err + } + + // TODO(ashankar,caprita): Use channels/locks to synchronize the + // setting and getting of exitErr. + var exitErr error + ns := starter.NamespaceArgs{ + PermissionsFile: filepath.Join(mtPermsDir, "acls"), + } + if testMode { + ns.ListenSpec = rpc.ListenSpec{Addrs: rpc.ListenAddrs{{"tcp", "127.0.0.1:0"}}} + } else { + ns.ListenSpec = v23.GetListenSpec(ctx) + ns.Name = *publishAs + ns.Neighborhood = *nhName + } + // TODO(caprita): Move pairing token generation and printing into the + // claimable service setup. + var pairingToken string + if *usePairingToken { + var token [8]byte + if _, err := rand.Read(token[:]); err != nil { + ctx.Errorf("unable to generate pairing token: %v", err) + return err + } + pairingToken = base64.URLEncoding.EncodeToString(token[:]) + ctx.VI(0).Infof("Device manager pairing token: %v", pairingToken) + ctx.FlushLog() + } + dev := starter.DeviceArgs{ + ConfigState: configState, + TestMode: testMode, + RestartCallback: func() { exitErr = cmdline.ErrExitCode(*restartExitCode) }, + PairingToken: pairingToken, + } + if testMode { + dev.ListenSpec = rpc.ListenSpec{Addrs: rpc.ListenAddrs{{"tcp", "127.0.0.1:0"}}} + } else { + if dev.ListenSpec, err = derivedListenSpec(ctx, ns.ListenSpec, *dmPort); err != nil { + return err + } + } + // We grab the shutdown channel at this point in order to ensure that we + // register a listener for the app cycle manager Stop before we start + // running the device manager service. Otherwise, any device manager + // method that calls Stop on the app cycle manager (e.g. the Stop RPC) + // will precipitate an immediate process exit. + shutdownChan := signals.ShutdownOnSignals(ctx) + _, stop, err := starter.Start(ctx, starter.Args{Namespace: ns, Device: dev, MountGlobalNamespaceInLocalNamespace: true}) + if err != nil { + return err + } + defer stop() + + // Wait until shutdown. Ignore duplicate signals (sent by agent and + // received as part of process group). + signals.SameSignalTimeWindow = 500 * time.Millisecond + ctx.Info("Shutting down due to: ", <-shutdownChan) + return exitErr +} + +// derivedListenSpec returns a copy of ls, with the ports changed to port. +func derivedListenSpec(ctx *context.T, ls rpc.ListenSpec, port int) (rpc.ListenSpec, error) { + orig := ls.Addrs + ls.Addrs = nil + for _, a := range orig { + host, _, err := net.SplitHostPort(a.Address) + if err != nil { + err = verror.New(errSplitHostPortFailed, ctx, a.Address, err) + ctx.Errorf(err.Error()) + return ls, err + } + a.Address = net.JoinHostPort(host, strconv.Itoa(port)) + ls.Addrs = append(ls.Addrs, a) + } + return ls, nil +} diff --git a/x/ref/services/device/inithelper/main.go b/x/ref/services/device/inithelper/main.go new file mode 100644 index 000000000..7e3301ee9 --- /dev/null +++ b/x/ref/services/device/inithelper/main.go @@ -0,0 +1,111 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Command inithelper manages services for a variety of platforms and init +// systems, such as upstart, systemd etc. +package main + +// TODO(caprita): The separation of responsibilities between root/non-root code +// can be shifted away from root a bit more, by having the init daemon files +// created as non-root and root just installs files and runs a specific set of +// commands. Figure out if worth it. Also consider combining with suidhelper. +// For now they're separate since we don't always need both at the same time. + +// TODO(caprita): Add unit test. + +import ( + "bytes" + "flag" + "fmt" + "os" + + "v.io/x/ref/services/device/internal/sysinit" +) + +func usage() { + const usage = `Usage: +%s [flags] [command] + + Flags: +%s + Command: + print: prints the file that would be installed + install: installs the service + uninstall: uninstalls the service + start: starts the service + stop: stops the service +` + var flagDefaults bytes.Buffer + flag.CommandLine.SetOutput(&flagDefaults) + flag.CommandLine.PrintDefaults() + flag.CommandLine.SetOutput(nil) + fmt.Fprintf(os.Stderr, usage, os.Args[0], flagDefaults.String()) +} + +func main() { + fmt.Fprintln(os.Stderr, os.Args) + if os.Geteuid() != 0 && os.Getuid() != 0 { + fmt.Fprintln(os.Stderr, "uid is ", os.Getuid(), ", effective uid is ", os.Geteuid()) + fmt.Fprintln(os.Stderr, "inithelper is not root. Is your filesystem mounted with nosuid?") + os.Exit(1) + } + + flag.Usage = usage + sdFlag := flag.String("service_description", "", "File containing a JSON-encoded sysinit.ServiceDescription object.") + systemFlag := flag.String("system", sysinit.InitSystem(), "System label, to select the appropriate sysinit mechanism.") + flag.Parse() + if *sdFlag == "" { + fmt.Fprintf(os.Stderr, "--service_description must be set.\n") + flag.Usage() + os.Exit(1) + } + if *systemFlag == "" { + fmt.Fprintf(os.Stderr, "--system must be set.\n") + flag.Usage() + os.Exit(1) + } + var sd sysinit.ServiceDescription + if err := sd.LoadFrom(*sdFlag); err != nil { + fmt.Fprintf(os.Stderr, "LoadFrom(%v) failed: %v\n", *sdFlag, err) + os.Exit(2) + } + si := sysinit.New(*systemFlag, &sd) + args := flag.Args() + if len(args) != 1 { + fmt.Fprintf(os.Stderr, "Command must be specified.\n") + flag.Usage() + os.Exit(1) + } + switch args[0] { + case "print": + if err := si.Print(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to print %v: %s\n", si, err) + os.Exit(2) + } + case "install": + if err := si.Install(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to install %v: %s\n", si, err) + os.Exit(2) + } + case "uninstall": + if err := si.Uninstall(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to uninstall %v: %s\n", si, err) + os.Exit(2) + } + case "start": + if err := si.Start(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to start %v: %s\n", si, err) + os.Exit(2) + } + case "stop": + if err := si.Stop(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to stop %v: %s\n", si, err) + os.Exit(2) + } + default: + fmt.Fprintf(os.Stderr, "Invalid command: %s\n", args[0]) + flag.Usage() + os.Exit(1) + } +} diff --git a/x/ref/services/device/internal/claim/claim.go b/x/ref/services/device/internal/claim/claim.go new file mode 100644 index 000000000..06667e147 --- /dev/null +++ b/x/ref/services/device/internal/claim/claim.go @@ -0,0 +1,125 @@ +// Copyright 2015 The Vanadium 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 claim + +import ( + "crypto/subtle" + "os" + "sync" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/device" + "v.io/v23/verror" + "v.io/x/ref/services/device/internal/errors" + "v.io/x/ref/services/internal/pathperms" +) + +// NewClaimableDispatcher returns an rpc.Dispatcher that allows the device to +// be Claimed if it hasn't been already and a channel that will be closed once +// the device has been claimed. +// +// It returns (nil, nil) if the device is no longer claimable. +func NewClaimableDispatcher(ctx *context.T, permsDir, pairingToken string, auth security.Authorizer) (rpc.Dispatcher, <-chan struct{}) { + permsStore := pathperms.NewPathStore(ctx) + if _, _, err := permsStore.Get(permsDir); !os.IsNotExist(err) { + // The device is claimable only if Claim hasn't been called before. The + // existence of the Permissions file is an indication of a successful prior + // call to Claim. + return nil, nil + } + notify := make(chan struct{}) + return &claimable{token: pairingToken, permsStore: permsStore, permsDir: permsDir, notify: notify, auth: auth}, notify +} + +// claimable implements the device.Claimable RPC interface and the +// rpc.Dispatcher and security.Authorizer to serve it. +// +// It allows the Claim RPC to be successfully invoked exactly once. +type claimable struct { + token string + permsStore *pathperms.PathStore + permsDir string + notify chan struct{} // GUARDED_BY(mu) + auth security.Authorizer + + // Lock used to ensure that a successful claim can happen at most once. + // This is done by allowing only a single goroutine to execute the + // meaty parts of Claim at a time. + mu sync.Mutex +} + +func (c *claimable) Claim(ctx *context.T, call rpc.ServerCall, pairingToken string) error { + // Verify that the claimer pairing tokens match that of the device manager. + if subtle.ConstantTimeCompare([]byte(pairingToken), []byte(c.token)) != 1 { + return verror.New(errors.ErrInvalidPairingToken, ctx) + } + var ( + granted = call.GrantedBlessings() // blessings granted by the claimant + principal = v23.GetPrincipal(ctx) + store = principal.BlessingStore() + ) + if granted.IsZero() { + return verror.New(errors.ErrInvalidBlessing, ctx) + } + c.mu.Lock() + defer c.mu.Unlock() + if c.notify == nil { + // Device has already been claimed (by a concurrent + // RPC perhaps), it cannot be reclaimed + return verror.New(errors.ErrDeviceAlreadyClaimed, ctx) + } + // TODO(ashankar): If the claim fails, would make sense + // to remove from roots as well. + if err := security.AddToRoots(principal, granted); err != nil { + return verror.New(errors.ErrInvalidBlessing, ctx) + } + if _, err := store.Set(granted, security.AllPrincipals); err != nil { + return verror.New(errors.ErrInvalidBlessing, ctx, err) + } + if err := store.SetDefault(granted); err != nil { + return verror.New(errors.ErrInvalidBlessing, ctx, err) + } + + // Create Permissions with all the granted blessings (which are now the default blessings) + // (irrespective of caveats). + patterns := security.DefaultBlessingPatterns(principal) + if len(patterns) == 0 { + return verror.New(errors.ErrInvalidBlessing, ctx) + } + + // Create Permissions that allow principals with the caller's blessings to + // administer and use the device. + perms := make(access.Permissions) + for _, bp := range patterns { + // TODO(caprita,ataly,ashankar): Do we really need the + // NonExtendable restriction below? + patterns := bp.MakeNonExtendable().PrefixPatterns() + for _, p := range patterns { + for _, tag := range access.AllTypicalTags() { + perms.Add(p, string(tag)) + } + } + } + if err := c.permsStore.Set(c.permsDir, perms, ""); err != nil { + return verror.New(errors.ErrOperationFailed, ctx) + } + ctx.Infof("Device claimed and Permissions set to: %v", perms) + close(c.notify) + c.notify = nil + return nil +} + +// TODO(ashankar): Remove this and use Serve instead of ServeDispatcher to setup +// the Claiming service. Shouldn't need the "device" suffix. +func (c *claimable) Lookup(_ *context.T, suffix string) (interface{}, security.Authorizer, error) { + if suffix != "" && suffix != "device" { + return nil, nil, verror.New(errors.ErrUnclaimedDevice, nil) + } + return device.ClaimableServer(c), c.auth, nil +} diff --git a/x/ref/services/device/internal/config/config.go b/x/ref/services/device/internal/config/config.go new file mode 100644 index 000000000..491c2e34e --- /dev/null +++ b/x/ref/services/device/internal/config/config.go @@ -0,0 +1,165 @@ +// Copyright 2015 The Vanadium 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 config handles configuration state passed across instances of the +// device manager. +// +// The State object captures setting that the device manager needs to be aware +// of when it starts. This is passed to the first invocation of the device +// manager, and then passed down from old device manager to new device manager +// upon update. The device manager has an implementation-dependent mechanism +// for parsing and passing state, which is encapsulated by the state sub-package +// (currently, the mechanism uses environment variables). When instantiating a +// new instance of the device manager service, the developer needs to pass in a +// copy of State. They can obtain this by calling Load, which captures any +// config state passed by a previous version of device manager during update. +// Any new version of the device manager must be able to decode a previous +// version's config state, even if the new version changes the mechanism for +// passing this state (that is, device manager implementations must be +// backward-compatible as far as accepting and passing config state goes). +// TODO(caprita): add config state versioning? +package config + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "v.io/v23/services/application" + "v.io/v23/verror" + "v.io/x/ref" +) + +const pkgPath = "v.io/x/ref/services/device/internal/config" + +var ( + errNeedName = verror.Register(pkgPath+".errNeedName", verror.NoRetry, "{1:}{2:} Name cannot be empty{:_}") + errNeedRoot = verror.Register(pkgPath+".errNeedRoot", verror.NoRetry, "{1:}{2:} Root cannot be empty{:_}") + errNeedCurrentLink = verror.Register(pkgPath+".errNeedCurrentLink", verror.NoRetry, "{1:}{2:} CurrentLink cannot be empty{:_}") + errNeedHelper = verror.Register(pkgPath+".errNeedHelper", verror.NoRetry, "{1:}{2:} Helper must be specified{:_}") + errCantDecodeEnvelope = verror.Register(pkgPath+".errCantDecodeEnvelope", verror.NoRetry, "{1:}{2:} failed to decode envelope from {3}{:_}") + errCantEncodeEnvelope = verror.Register(pkgPath+".errCantEncodeEnvelope", verror.NoRetry, "{1:}{2:} failed to encode envelope {3}{:_}") + errEvalSymlinksFailed = verror.Register(pkgPath+".errEvalSymlinksFailed", verror.NoRetry, "{1:}{2:} EvalSymlinks failed{:_}") +) + +// State specifies how the device manager is configured. This should +// encapsulate what the device manager needs to know and/or be able to mutate +// about its environment. +type State struct { + // Name is the device manager's object name. Must be non-empty. + Name string + // Envelope is the device manager's application envelope. If nil, any + // envelope fetched from the application repository will trigger an + // update. + Envelope *application.Envelope + // Previous holds the local path to the previous version of the device + // manager. If empty, revert is disabled. + Previous string + // Root is the directory on the local filesystem that contains + // the applications' workspaces. Must be non-empty. + Root string + // Origin is the application repository object name for the device + // manager application. If empty, update is disabled. + Origin string + // CurrentLink is the local filesystem soft link that should point to + // the version of the device manager binary/script through which device + // manager is started. Device manager is expected to mutate this during + // a self-update. Must be non-empty. + CurrentLink string + // Helper is the path to the setuid helper for running applications as + // specific users. + Helper string +} + +// Validate checks the config state. +func (c *State) Validate() error { + if c.Name == "" { + return verror.New(errNeedName, nil) + } + if c.Root == "" { + return verror.New(errNeedRoot, nil) + } + if c.CurrentLink == "" { + return verror.New(errNeedCurrentLink, nil) + } + if c.Helper == "" { + return verror.New(errNeedHelper, nil) + } + return nil +} + +// Load reconstructs the config state passed to the device manager (presumably +// by the parent device manager during an update). Currently, this is done via +// environment variables. +func Load() (*State, error) { + var env *application.Envelope + if jsonEnvelope := os.Getenv(EnvelopeEnv); jsonEnvelope != "" { + env = new(application.Envelope) + if err := json.Unmarshal([]byte(jsonEnvelope), env); err != nil { + return nil, verror.New(errCantDecodeEnvelope, nil, jsonEnvelope, err) + } + } + return &State{ + Envelope: env, + Previous: os.Getenv(PreviousEnv), + Root: os.Getenv(RootEnv), + Origin: os.Getenv(OriginEnv), + CurrentLink: os.Getenv(CurrentLinkEnv), + Helper: os.Getenv(HelperEnv), + }, nil +} + +// Save serializes the config state meant to be passed to a child device manager +// during an update, returning a slice of "key=value" strings, which are +// expected to be stuffed into environment variable settings by the caller. +func (c *State) Save(envelope *application.Envelope) ([]string, error) { + var jsonEnvelope []byte + if envelope != nil { + var err error + if jsonEnvelope, err = json.Marshal(envelope); err != nil { + return nil, verror.New(errCantEncodeEnvelope, nil, envelope, err) + } + } + var currScript string + if _, err := os.Lstat(c.CurrentLink); !os.IsNotExist(err) { + if currScript, err = filepath.EvalSymlinks(c.CurrentLink); err != nil { + return nil, verror.New(errEvalSymlinksFailed, nil, err) + } + } + settings := map[string]string{ + EnvelopeEnv: string(jsonEnvelope), + PreviousEnv: currScript, + RootEnv: c.Root, + OriginEnv: c.Origin, + CurrentLinkEnv: c.CurrentLink, + HelperEnv: c.Helper, + } + // We need to manually pass the namespace roots to the child, since we + // currently don't have a way for the child to obtain this information + // from a config service at start-up. + roots, _ := ref.EnvNamespaceRoots() + var ret []string + for k, v := range roots { + ret = append(ret, k+"="+v) + } + for k, v := range settings { + ret = append(ret, k+"="+v) + } + return ret, nil +} + +// QuoteEnv wraps environment variable values in double quotes, making them +// suitable for inclusion in a bash script. +func QuoteEnv(env []string) (ret []string) { + for _, e := range env { + if eqIdx := strings.Index(e, "="); eqIdx > 0 { + ret = append(ret, fmt.Sprintf("%s=%q", e[:eqIdx], e[eqIdx+1:])) + } else { + ret = append(ret, e) + } + } + return +} diff --git a/x/ref/services/device/internal/config/config_test.go b/x/ref/services/device/internal/config/config_test.go new file mode 100644 index 000000000..968bda88c --- /dev/null +++ b/x/ref/services/device/internal/config/config_test.go @@ -0,0 +1,135 @@ +// Copyright 2015 The Vanadium 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 config_test + +import ( + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "v.io/x/ref/services/device/internal/config" + + "v.io/v23/services/application" +) + +// TestState checks that encoding/decoding State to child/from parent works +// as expected. +func TestState(t *testing.T) { + var err error + currScript := filepath.Join(os.TempDir(), "fido/was/here") + if err := os.MkdirAll(currScript, 0700); err != nil { + t.Fatalf("MkdirAll(%v) failed: %v", currScript, err) + } + defer os.RemoveAll(currScript) + // On some operating systems (e.g. darwin) os.TempDir() can + // return a symlink. To avoid having to account for this + // eventuality later, evaluate the symlink. + currScript, err = filepath.EvalSymlinks(currScript) + if err != nil { + t.Fatalf("EvalSymlinks() failed: %v", err) + } + currLink := filepath.Join(os.TempDir(), "familydog") + if err := os.Symlink(currScript, currLink); err != nil { + t.Fatalf("Symlink(%v, %v) failed: %v", currScript, currLink, err) + } + defer os.Remove(currLink) + // For the same reasons mentioned above, evaluate the symlink. + currLink, err = filepath.EvalSymlinks(currLink) + if err != nil { + t.Fatalf("EvalSymlinks() failed: %v", err) + } + state := &config.State{ + Name: "fido", + Previous: "doesn't matter", + Root: "fidos/doghouse", + Origin: "pet/store", + CurrentLink: currLink, + Helper: "santas/little/helper", + } + if err := state.Validate(); err != nil { + t.Errorf("Config state %v failed to validate: %v", state, err) + } + encoded, err := state.Save(&application.Envelope{ + Title: "dog", + Args: []string{"roll-over", "play-dead"}, + }) + if err != nil { + t.Errorf("Config state %v Save failed: %v", state, err) + } + for _, e := range encoded { + pair := strings.SplitN(e, "=", 2) + os.Setenv(pair[0], pair[1]) + } + decodedState, err := config.Load() + if err != nil { + t.Errorf("Config state Load failed: %v", err) + } + expectedState := state + expectedState.Envelope = &application.Envelope{ + Title: "dog", + Args: []string{"roll-over", "play-dead"}, + } + expectedState.Name = "" + expectedState.Previous = currScript + if !reflect.DeepEqual(decodedState, expectedState) { + t.Errorf("Decode state: want %#v, got %#v", expectedState, decodedState) + } +} + +// TestValidate checks the Validate method of State. +func TestValidate(t *testing.T) { + state := &config.State{ + Name: "schweinsteiger", + Previous: "a", + Root: "b", + Origin: "c", + CurrentLink: "d", + Helper: "e", + } + if err := state.Validate(); err != nil { + t.Errorf("Config state %v failed to validate: %v", state, err) + } + state.Root = "" + if err := state.Validate(); err == nil { + t.Errorf("Config state %v should have failed to validate.", state) + } + state.Root, state.CurrentLink = "a", "" + if err := state.Validate(); err == nil { + t.Errorf("Confi stateg %v should have failed to validate.", state) + } + state.CurrentLink, state.Name = "d", "" + if err := state.Validate(); err == nil { + t.Errorf("Config state %v should have failed to validate.", state) + } + state.Name, state.Helper = "anything", "" + if err := state.Validate(); err == nil { + t.Errorf("Config state %v should have failed to validate.", state) + } +} + +// TestQuoteEnv checks the QuoteEnv method. +func TestQuoteEnv(t *testing.T) { + cases := []struct { + before, after string + }{ + {`a=b`, `a="b"`}, + {`a=`, `a=""`}, + {`a`, `a`}, + {`a=x y`, `a="x y"`}, + {`a="x y"`, `a="\"x y\""`}, + {`a='x y'`, `a="'x y'"`}, + } + var input []string + var want []string + for _, c := range cases { + input = append(input, c.before) + want = append(want, c.after) + } + if got := config.QuoteEnv(input); !reflect.DeepEqual(want, got) { + t.Errorf("QuoteEnv(%v) wanted %v, got %v instead", input, want, got) + } +} diff --git a/x/ref/services/device/internal/config/const.go b/x/ref/services/device/internal/config/const.go new file mode 100644 index 000000000..e6d33ba4f --- /dev/null +++ b/x/ref/services/device/internal/config/const.go @@ -0,0 +1,28 @@ +// Copyright 2015 The Vanadium 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 config + +const ( + // EnvelopeEnv is the name of the environment variable that holds the + // serialized device manager application envelope. + EnvelopeEnv = "V23_DM_ENVELOPE" + // PreviousEnv is the name of the environment variable that holds the + // path to the previous version of the device manager. + PreviousEnv = "V23_DM_PREVIOUS" + // OriginEnv is the name of the environment variable that holds the + // object name of the application repository that can be used to + // retrieve the device manager application envelope. + OriginEnv = "V23_DM_ORIGIN" + // RootEnv is the name of the environment variable that holds the + // path to the directory in which device manager workspaces are + // created. + RootEnv = "V23_DM_ROOT" + // CurrentLinkEnv is the name of the environment variable that holds + // the path to the soft link that points to the current device manager. + CurrentLinkEnv = "V23_DM_CURRENT" + // HelperEnv is the name of the environment variable that holds the path + // to the suid helper used to start apps as specific system users. + HelperEnv = "V23_DM_HELPER" +) diff --git a/x/ref/services/device/internal/errors/errors.go b/x/ref/services/device/internal/errors/errors.go new file mode 100644 index 000000000..f268f187c --- /dev/null +++ b/x/ref/services/device/internal/errors/errors.go @@ -0,0 +1,31 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// TODO(caprita): Consider moving these to v23 if they're meant to be public +// beyond the deviced and device tool implementations. + +// Package errors defines the error ids that are shared between server and +// client-side. +package errors + +import "v.io/v23/verror" + +// TODO(caprita): the value of pkgPath corresponds to the previous package where +// the error ids were defined. Updating error ids needs to be carefully +// coordinated between clients and servers, so we should do it when we settle on +// the final location for these error definitions. +const pkgPath = "v.io/x/ref/services/device/internal/impl" + +var ( + ErrInvalidSuffix = verror.Register(pkgPath+".InvalidSuffix", verror.NoRetry, "{1:}{2:} invalid suffix{:_}") + ErrOperationFailed = verror.Register(pkgPath+".OperationFailed", verror.NoRetry, "{1:}{2:} operation failed{:_}") + ErrOperationInProgress = verror.Register(pkgPath+".OperationInProgress", verror.NoRetry, "{1:}{2:} operation in progress{:_}") + ErrAppTitleMismatch = verror.Register(pkgPath+".AppTitleMismatch", verror.NoRetry, "{1:}{2:} app title mismatch{:_}") + ErrUpdateNoOp = verror.Register(pkgPath+".UpdateNoOp", verror.NoRetry, "{1:}{2:} update is no op{:_}") + ErrInvalidOperation = verror.Register(pkgPath+".InvalidOperation", verror.NoRetry, "{1:}{2:} invalid operation{:_}") + ErrInvalidBlessing = verror.Register(pkgPath+".InvalidBlessing", verror.NoRetry, "{1:}{2:} invalid blessing{:_}") + ErrInvalidPairingToken = verror.Register(pkgPath+".InvalidPairingToken", verror.NoRetry, "{1:}{2:} pairing token mismatch{:_}") + ErrUnclaimedDevice = verror.Register(pkgPath+".UnclaimedDevice", verror.NoRetry, "{1:}{2:} device needs to be claimed first") + ErrDeviceAlreadyClaimed = verror.Register(pkgPath+".AlreadyClaimed", verror.NoRetry, "{1:}{2:} device has already been claimed") +) diff --git a/x/ref/services/device/internal/suid/args.go b/x/ref/services/device/internal/suid/args.go new file mode 100644 index 000000000..8ba2eebc0 --- /dev/null +++ b/x/ref/services/device/internal/suid/args.go @@ -0,0 +1,234 @@ +// Copyright 2015 The Vanadium 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 suid + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "os" + "os/user" + "strconv" + "strings" + + "v.io/v23/verror" +) + +const pkgPath = "v.io/x/ref/services/device/internal/suid" + +var ( + errUserNameMissing = verror.Register(pkgPath+".errUserNameMissing", verror.NoRetry, "{1:}{2:} --username missing{:_}") + errUnknownUser = verror.Register(pkgPath+".errUnknownUser", verror.NoRetry, "{1:}{2:} --username {3}: unknown user{:_}") + errInvalidUID = verror.Register(pkgPath+".errInvalidUID", verror.NoRetry, "{1:}{2:} user.Lookup() returned an invalid uid {3}{:_}") + errInvalidGID = verror.Register(pkgPath+".errInvalidGID", verror.NoRetry, "{1:}{2:} user.Lookup() returned an invalid gid {3}{:_}") + errUIDTooLow = verror.Register(pkgPath+".errUIDTooLow", verror.NoRetry, "{1:}{2:} suidhelper uid {3} is not permitted because it is less than {4}{:_}") + errAtoiFailed = verror.Register(pkgPath+".errAtoiFailed", verror.NoRetry, "{1:}{2:} strconv.Atoi({3}) failed{:_}") + errInvalidFlags = verror.Register(pkgPath+".errInvalidFlags", verror.NoRetry, "{1:}{2:} invalid flags ({3} are set){:_}") +) + +type WorkParameters struct { + uid int + gid int + workspace string + agentsock string + logDir string + argv0 string + argv []string + envv []string + dryrun bool + remove bool + chown bool + kill bool + killPids []int +} + +type ArgsSavedForTest struct { + Uname string + Workpace string + Run string + LogDir string +} + +const SavedArgs = "V23_SAVED_ARGS" + +var ( + flagUsername, flagWorkspace, flagLogDir, flagRun, flagProgName, flagAgentSock *string + flagMinimumUid *int64 + flagRemove, flagKill, flagChown, flagDryrun *bool +) + +func init() { + setupFlags(flag.CommandLine) +} + +func setupFlags(fs *flag.FlagSet) { + const uidThreshold = 501 + flagUsername = fs.String("username", "", "The UNIX user name used for the other functions of this tool.") + flagWorkspace = fs.String("workspace", "", "Path to the application's workspace directory.") + flagAgentSock = fs.String("agentsock", "", "Path to the application's security agent socket.") + flagLogDir = fs.String("logdir", "", "Path to the log directory.") + flagRun = fs.String("run", "", "Path to the application to exec.") + flagProgName = fs.String("progname", "unnamed_app", "Visible name of the application, used in argv[0]") + flagMinimumUid = fs.Int64("minuid", uidThreshold, "UIDs cannot be less than this number.") + flagRemove = fs.Bool("rm", false, "Remove the file trees given as command-line arguments.") + flagKill = fs.Bool("kill", false, "Kill process ids given as command-line arguments.") + flagChown = fs.Bool("chown", false, "Change owner of files and directories given as command-line arguments to the user specified by this flag") + flagDryrun = fs.Bool("dryrun", false, "Elides root-requiring systemcalls.") +} + +func cleanEnv(env []string) []string { + nenv := []string{} + for _, e := range env { + if !strings.HasPrefix(e, "V23_SUIDHELPER_TEST") { + nenv = append(nenv, e) + } + } + return nenv +} + +// checkFlagCombinations makes sure that a valid combination of flags has been +// specified for rm/kill/chown +// +// --rm and --kill are modal. Complain if any other flag is set along with one of +// those. --chown allows specification of --username, --dryrun, and --minuid, +// but nothing else +func checkFlagCombinations(fs *flag.FlagSet) error { + if !(*flagRemove || *flagKill || *flagChown) { + return nil + } + + // Count flags that are set. The device manager test always sets --minuid=1 + // and --test.run=TestSuidHelper so when in a test, tolerate those. + flagsToIgnore := map[string]string{} + if os.Getenv("V23_SUIDHELPER_TEST") != "" { + flagsToIgnore["minuid"] = "1" + flagsToIgnore["test.run"] = "TestSuidHelper" + } + if *flagChown { + // Allow all values of --username, --dryrun, and --minuid + flagsToIgnore["username"] = "*" + flagsToIgnore["dryrun"] = "*" + flagsToIgnore["minuid"] = "*" + } + + counter := 0 + fs.Visit(func(f *flag.Flag) { + if flagsToIgnore[f.Name] != f.Value.String() && flagsToIgnore[f.Name] != "*" { + counter++ + } + }) + + if counter > 1 { + return verror.New(errInvalidFlags, nil, counter, "--rm and --kill cannot be used with any other flag. --chown can only be used with --username, --dryrun, and --minuid") + } + return nil +} + +// warnMissingSuidPrivs makes it a little easier to debug when suid privs are required but +// are not present. It's not a comprehensive check -- e.g. we may be running as user +// <username> and suppress the warning, but still fail to chown a file owned by some other user. +func warnMissingSuidPrivs(uid int) { + osUid, osEuid := os.Getuid(), os.Geteuid() + if osUid == 0 || osEuid == 0 || osUid == uid || osEuid == uid { + return + } + + fmt.Fprintln(os.Stderr, "uid is ", os.Getuid(), ", effective uid is ", os.Geteuid()) + fmt.Fprintln(os.Stderr, "WARNING: suidhelper is not root. Is your filesystem mounted with nosuid?") +} + +// ParseArguments populates the WorkParameter object from the provided args +// and env strings. +func (wp *WorkParameters) ProcessArguments(fs *flag.FlagSet, env []string) error { + if err := checkFlagCombinations(fs); err != nil { + return err + } + + if *flagRemove { + wp.remove = true + wp.argv = fs.Args() + return nil + } + + if *flagKill { + wp.kill = true + for _, p := range fs.Args() { + pid, err := strconv.Atoi(p) + if err != nil { + wp.killPids = nil + return verror.New(errAtoiFailed, nil, p, err) + } + wp.killPids = append(wp.killPids, pid) + } + return nil + } + + if *flagDryrun { + wp.uid, wp.gid = -1, -1 + } else { + username := *flagUsername + if username == "" { + return verror.New(errUserNameMissing, nil) + } + + usr, err := user.Lookup(username) + if err != nil { + return verror.New(errUnknownUser, nil, username) + } + + uid, err := strconv.ParseInt(usr.Uid, 0, 32) + if err != nil { + return verror.New(errInvalidUID, nil, usr.Uid) + } + gid, err := strconv.ParseInt(usr.Gid, 0, 32) + if err != nil { + return verror.New(errInvalidGID, nil, usr.Gid) + } + warnMissingSuidPrivs(int(uid)) + + // Uids less than 501 can be special so we forbid running as them. + if uid < *flagMinimumUid { + return verror.New(errUIDTooLow, nil, + uid, *flagMinimumUid) + } + wp.uid = int(uid) + wp.gid = int(gid) + } + wp.dryrun = *flagDryrun + + // At this point, all flags allowed by --chown have been processed + if *flagChown { + wp.chown = true + wp.argv = fs.Args() + return nil + } + + // Preserve the arguments for examination by the test harness if executed + // in the course of a test. + if os.Getenv("V23_SUIDHELPER_TEST") != "" { + env = cleanEnv(env) + b := new(bytes.Buffer) + enc := json.NewEncoder(b) + enc.Encode(ArgsSavedForTest{ + Uname: *flagUsername, + Workpace: *flagWorkspace, + Run: *flagRun, + LogDir: *flagLogDir, + }) + env = append(env, SavedArgs+"="+b.String()) + wp.dryrun = true + } + + wp.workspace = *flagWorkspace + wp.agentsock = *flagAgentSock + wp.argv0 = *flagRun + wp.logDir = *flagLogDir + wp.argv = append([]string{*flagProgName}, fs.Args()...) + // TODO(rjkroege): Reduce the environment to the absolute minimum needed. + wp.envv = env + + return nil +} diff --git a/x/ref/services/device/internal/suid/args_test.go b/x/ref/services/device/internal/suid/args_test.go new file mode 100644 index 000000000..d235cc5a2 --- /dev/null +++ b/x/ref/services/device/internal/suid/args_test.go @@ -0,0 +1,207 @@ +// Copyright 2015 The Vanadium 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 suid + +import ( + "flag" + "reflect" + "testing" + + "v.io/v23/verror" +) + +// Note: The specific user chosen has no consequence other than it has the same +// ids across the set of systems we are testing. +const ( + testUserName = "daemon" + testUid = 1 + testGid = 1 +) + +func TestParseArguments(t *testing.T) { + cases := []struct { + cmdline []string + env []string + errID verror.ID + expected WorkParameters + }{ + + { + []string{"setuidhelper"}, + []string{}, + errUserNameMissing.ID, + WorkParameters{}, + }, + + { + []string{"setuidhelper", "--minuid", "1", "--username", testUserName}, + []string{"A=B"}, + "", + WorkParameters{ + uid: testUid, + gid: testGid, + workspace: "", + agentsock: "", + logDir: "", + argv0: "", + argv: []string{"unnamed_app"}, + envv: []string{"A=B"}, + dryrun: false, + remove: false, + chown: false, + kill: false, + killPids: nil, + }, + }, + + { + []string{"setuidhelper", "--minuid", "1", "--username", testUserName, "--workspace", "/hello", + "--logdir", "/logging", "--agentsock", "/tmp/sXXXX", "--run", "/bin/v23", "--", "one", "two"}, + []string{"A=B"}, + "", + WorkParameters{ + uid: testUid, + gid: testGid, + workspace: "/hello", + agentsock: "/tmp/sXXXX", + logDir: "/logging", + argv0: "/bin/v23", + argv: []string{"unnamed_app", "one", "two"}, + envv: []string{"A=B"}, + dryrun: false, + remove: false, + chown: false, + kill: false, + killPids: nil, + }, + }, + + { + []string{"setuidhelper", "--username", testUserName}, + []string{"A=B"}, + errUIDTooLow.ID, + WorkParameters{}, + }, + + { + []string{"setuidhelper", "--rm", "hello", "vanadium"}, + []string{"A=B"}, + "", + WorkParameters{ + uid: 0, + gid: 0, + workspace: "", + agentsock: "", + logDir: "", + argv0: "", + argv: []string{"hello", "vanadium"}, + envv: nil, + dryrun: false, + remove: true, + chown: false, + kill: false, + killPids: nil, + }, + }, + + { + []string{"setuidhelper", "--chown", "--username", testUserName, "--dryrun", "--minuid", "1", "/tmp/foo", "/tmp/bar"}, + []string{"A=B"}, + "", + WorkParameters{ + uid: -1, + gid: -1, + workspace: "", + agentsock: "", + logDir: "", + argv0: "", + argv: []string{"/tmp/foo", "/tmp/bar"}, + envv: nil, + dryrun: true, + remove: false, + chown: true, + kill: false, + killPids: nil, + }, + }, + + { + []string{"setuidhelper", "--kill", "235", "451"}, + []string{"A=B"}, + "", + WorkParameters{ + uid: 0, + gid: 0, + workspace: "", + agentsock: "", + logDir: "", + argv0: "", + argv: nil, + envv: nil, + dryrun: false, + remove: false, + chown: false, + kill: true, + killPids: []int{235, 451}, + }, + }, + + { + []string{"setuidhelper", "--kill", "235", "451oops"}, + []string{"A=B"}, + errAtoiFailed.ID, + WorkParameters{ + uid: 0, + gid: 0, + workspace: "", + agentsock: "", + logDir: "", + argv0: "", + argv: nil, + envv: nil, + dryrun: false, + remove: false, + chown: false, + kill: true, + killPids: nil, + }, + }, + + { + []string{"setuidhelper", "--minuid", "1", "--username", testUserName, "--workspace", "/hello", "--progname", "binaryd/vanadium/app/testapp", + "--logdir", "/logging", "--agentsock", "/tmp/2981298123/s", "--run", "/bin/v23", "--dryrun", "--", "one", "two"}, + []string{"A=B"}, + "", + WorkParameters{ + uid: -1, + gid: -1, + workspace: "/hello", + agentsock: "/tmp/2981298123/s", + logDir: "/logging", + argv0: "/bin/v23", + argv: []string{"binaryd/vanadium/app/testapp", "one", "two"}, + envv: []string{"A=B"}, + dryrun: true, + remove: false, + chown: false, + kill: false, + killPids: nil, + }, + }, + } + + for _, c := range cases { + var wp WorkParameters + fs := flag.NewFlagSet(c.cmdline[0], flag.ExitOnError) + setupFlags(fs) + fs.Parse(c.cmdline[1:]) + if err := wp.ProcessArguments(fs, c.env); (err != nil || c.errID != "") && verror.ErrorID(err) != c.errID { + t.Fatalf("got %s (%v), expected %q error", verror.ErrorID(err), err, c.errID) + } + if !reflect.DeepEqual(wp, c.expected) { + t.Fatalf("got %#v expected %#v", wp, c.expected) + } + } +} diff --git a/x/ref/services/device/internal/suid/constants.go b/x/ref/services/device/internal/suid/constants.go new file mode 100644 index 000000000..19e21b743 --- /dev/null +++ b/x/ref/services/device/internal/suid/constants.go @@ -0,0 +1,11 @@ +// Copyright 2015 The Vanadium 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 suid + +const ( + // fd of the pipe to be used to return the pid of the forked child to the + // device manager. + PipeToParentFD = 3 +) diff --git a/x/ref/services/device/internal/suid/run.go b/x/ref/services/device/internal/suid/run.go new file mode 100644 index 000000000..05d85d4d8 --- /dev/null +++ b/x/ref/services/device/internal/suid/run.go @@ -0,0 +1,36 @@ +// Copyright 2015 The Vanadium 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 suid + +import ( + "flag" +) + +func Run(environ []string) error { + var work WorkParameters + if err := work.ProcessArguments(flag.CommandLine, environ); err != nil { + return err + } + + if work.remove { + return work.Remove() + } + + if work.kill { + return work.Kill() + } + + if err := work.Chown(); err != nil { + return err + } + + if work.chown { + // We were called with --chown, and Chown() was called above. + // There is nothing else to do. + return nil + } + + return work.Exec() +} diff --git a/x/ref/services/device/internal/suid/system.go b/x/ref/services/device/internal/suid/system.go new file mode 100644 index 000000000..8f2f3dd3e --- /dev/null +++ b/x/ref/services/device/internal/suid/system.go @@ -0,0 +1,140 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build linux darwin + +package suid + +import ( + "encoding/binary" + "log" + "os" + "path/filepath" + "syscall" + + "v.io/v23/verror" +) + +var ( + errChownFailed = verror.Register(pkgPath+".errChownFailed", verror.NoRetry, "{1:}{2:} os.Chown({3}, {4}, {5}) failed{:_}") + errGetwdFailed = verror.Register(pkgPath+".errGetwdFailed", verror.NoRetry, "{1:}{2:} os.Getwd failed{:_}") + errStartProcessFailed = verror.Register(pkgPath+".errStartProcessFailed", verror.NoRetry, "{1:}{2:} syscall.StartProcess({3}) failed{:_}") + errRemoveAllFailed = verror.Register(pkgPath+".errRemoveAllFailed", verror.NoRetry, "{1:}{2:} os.RemoveAll({3}) failed{:_}") + errFindProcessFailed = verror.Register(pkgPath+".errFindProcessFailed", verror.NoRetry, "{1:}{2:} os.FindProcess({3}) failed{:_}") + errKillFailed = verror.Register(pkgPath+".errKillFailed", verror.NoRetry, "{1:}{2:} os.Process.Kill({3}) failed{:_}") +) + +// Chown is only availabe on UNIX platforms so this file has a build +// restriction. +func (hw *WorkParameters) Chown() error { + chown := func(path string, _ os.FileInfo, inerr error) error { + if inerr != nil { + return inerr + } + if hw.dryrun { + log.Printf("[dryrun] os.Chown(%s, %d, %d)", path, hw.uid, hw.gid) + return nil + } + return os.Chown(path, hw.uid, hw.gid) + } + + chownPaths := hw.argv + if !hw.chown { + // Chown was invoked as part of regular suid execution, rather than directly + // via --chown. In that case, we chown the workspace, log directory, and, + // if specified, the agent socket path + // TODO(rjkroege): Ensure that the device manager can read log entries. + chownPaths = []string{hw.workspace, hw.logDir} + if hw.agentsock != "" { + chownPaths = append(chownPaths, hw.agentsock) + } + } + + for _, p := range chownPaths { + if err := filepath.Walk(p, chown); err != nil { + return verror.New(errChownFailed, nil, p, hw.uid, hw.gid, err) + } + } + return nil +} + +func (hw *WorkParameters) Exec() error { + attr := new(syscall.ProcAttr) + + dir, err := os.Getwd() + if err != nil { + log.Printf("error Getwd(): %v", err) + return verror.New(errGetwdFailed, nil, err) + } + attr.Dir = dir + attr.Env = hw.envv + attr.Files = []uintptr{ + uintptr(syscall.Stdin), + uintptr(syscall.Stdout), + uintptr(syscall.Stderr), + } + + attr.Sys = new(syscall.SysProcAttr) + attr.Sys.Setsid = true + if hw.dryrun { + log.Printf("[dryrun] syscall.Setgid(%d)", hw.gid) + log.Printf("[dryrun] syscall.Setuid(%d)", hw.uid) + } else if syscall.Getuid() != hw.uid || syscall.Getgid() != hw.gid { + attr.Sys.Credential = new(syscall.Credential) + attr.Sys.Credential.Gid = uint32(hw.gid) + attr.Sys.Credential.Uid = uint32(hw.uid) + } + + // Make sure the child won't talk on the fd we use to talk back to the parent + syscall.CloseOnExec(PipeToParentFD) + + // Start the child process + pid, _, err := syscall.StartProcess(hw.argv0, hw.argv, attr) + if err != nil { + if !hw.dryrun { + log.Printf("StartProcess failed: argv: %q %#v attr: %#v, attr.Sys: %#v, attr.Sys.Cred: %#v error: %v", hw.argv0, hw.argv, attr, attr.Sys, attr.Sys.Credential, err) + } else { + log.Printf("StartProcess failed: %v", err) + } + return verror.New(errStartProcessFailed, nil, hw.argv0, err) + } + + // Return the pid of the new child process + pipeToParent := os.NewFile(PipeToParentFD, "pipe_to_parent_wr") + if err = binary.Write(pipeToParent, binary.LittleEndian, int32(pid)); err != nil { + log.Printf("Problem returning pid to parent: %v", err) + } else { + log.Printf("Returned pid %v to parent", pid) + } + + os.Exit(0) + return nil // Not reached. +} + +func (hw *WorkParameters) Remove() error { + for _, p := range hw.argv { + if err := os.RemoveAll(p); err != nil { + return verror.New(errRemoveAllFailed, nil, p, err) + } + } + return nil +} + +func (hw *WorkParameters) Kill() error { + for _, pid := range hw.killPids { + + switch err := syscall.Kill(pid, 9); err { + case syscall.ESRCH: + // No such PID. + log.Printf("process pid %d already killed", pid) + case nil: + log.Printf("process pid %d killed successfully", pid) + default: + // Something went wrong. + return verror.New(errKillFailed, nil, pid, err) + } + + } + return nil +} diff --git a/x/ref/services/device/internal/suid/system_test.go b/x/ref/services/device/internal/suid/system_test.go new file mode 100644 index 000000000..98c30910e --- /dev/null +++ b/x/ref/services/device/internal/suid/system_test.go @@ -0,0 +1,74 @@ +// Copyright 2015 The Vanadium 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 suid + +import ( + "bytes" + "io/ioutil" + "log" + "os" + "path" + "testing" +) + +func TestChown(t *testing.T) { + dir, err := ioutil.TempDir("", "chown_test") + if err != nil { + t.Fatalf("ioutil.TempDir() failed: %v", err) + } + defer os.RemoveAll(dir) + + for _, p := range []string{"a/b/c", "c/d"} { + fp := path.Join(dir, p) + if err := os.MkdirAll(fp, os.FileMode(0700)); err != nil { + t.Fatalf("os.MkdirAll(%s) failed: %v", fp, err) + } + } + + wp := &WorkParameters{ + uid: 42, + gid: 7, + logDir: path.Join(dir, "a"), + workspace: path.Join(dir, "c"), + + dryrun: true, + } + + // Collect the log entries. + b := new(bytes.Buffer) + log.SetOutput(b) + log.SetFlags(0) + defer log.SetOutput(os.Stderr) + defer log.SetFlags(log.LstdFlags) + + // Mock-chown the tree. + if err := wp.Chown(); err != nil { + t.Fatalf("wp.Chown() wrongly failed: %v", err) + } + + // Edit the log buffer to remove the invocation dependent output. + pb := bytes.TrimSpace(bytes.Replace(b.Bytes(), []byte(dir), []byte("$PATH"), -1)) + + cmds := bytes.Split(pb, []byte{'\n'}) + for i, _ := range cmds { + cmds[i] = bytes.TrimSpace(cmds[i]) + } + + expected := []string{ + "[dryrun] os.Chown($PATH/c, 42, 7)", + "[dryrun] os.Chown($PATH/c/d, 42, 7)", + "[dryrun] os.Chown($PATH/a, 42, 7)", + "[dryrun] os.Chown($PATH/a/b, 42, 7)", + "[dryrun] os.Chown($PATH/a/b/c, 42, 7)", + } + if got, expected := len(cmds), len(expected); got != expected { + t.Fatalf("bad length. got: %d, expected %d", got, expected) + } + for i, _ := range expected { + if expected, got := expected[i], string(cmds[i]); expected != got { + t.Fatalf("wp.Chown output %d: got %v, expected %v", i, got, expected) + } + } +} diff --git a/x/ref/services/device/internal/sysinit/init_darwin.go b/x/ref/services/device/internal/sysinit/init_darwin.go new file mode 100644 index 000000000..b1ae82278 --- /dev/null +++ b/x/ref/services/device/internal/sysinit/init_darwin.go @@ -0,0 +1,13 @@ +// Copyright 2015 The Vanadium 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 sysinit + +func InitSystem() string { + panic("Darwin not supported yet") +} + +func New(system string, sd *ServiceDescription) InstallSystemInit { + panic("Darwin not supported yet") +} diff --git a/x/ref/services/device/internal/sysinit/init_linux.go b/x/ref/services/device/internal/sysinit/init_linux.go new file mode 100644 index 000000000..1d41ba60e --- /dev/null +++ b/x/ref/services/device/internal/sysinit/init_linux.go @@ -0,0 +1,319 @@ +// Copyright 2015 The Vanadium 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 sysinit + +// TODO(cnicolaou): will need to figure out a simple of way of handling the +// different init systems supported by various versions of linux. One simple +// option is to just include them in the name when installing - e.g. +// simplevns-upstart. + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "syscall" +) + +// action is a var so we can override it for testing. +var action = func(command, action, service string) error { + cmd := exec.Command(command, action, service) + if os.Geteuid() == 0 && os.Getuid() > 0 { + // Set uid to root (e.g. when running from a suid binary), + // otherwise initctl doesn't work. + sysProcAttr := new(syscall.SysProcAttr) + sysProcAttr.Credential = new(syscall.Credential) + sysProcAttr.Credential.Gid = uint32(0) + sysProcAttr.Credential.Uid = uint32(0) + cmd.SysProcAttr = sysProcAttr + } + // Clear env. In particular, initctl doesn't like USER being set to + // something other than root. + cmd.Env = []string{} + output, err := cmd.CombinedOutput() + fmt.Fprintf(os.Stderr, "%s output: for %s %s: %s\n", command, action, service, output) + return err +} + +var ( + upstartDir = "/etc/init" + upstartBin = "/sbin/initctl" + systemdDir = "/lib/systemd/system" // This works for both rpi and edison (/usr/lib does not) + systemdTmpFileDir = "/usr/lib/tmpfiles.d" + dockerDir = "/home/veyron/init" +) + +// InitSystem attempts to determine what kind of init system is in use on +// the platform that it is run on. It recognises upstart and systemd by +// testing for the presence of the initctl and systemctl commands. upstart +// is tested for first and hence is preferred in the unlikely case that both +// are installed. Docker containers do not support upstart and systemd and +// for them we have our own init system that uses the daemon command to +// start/stop/respawn jobs. +func InitSystem() string { + // NOTE(spetrovic): This check is kind of a hack. Ideally, we would + // detect a docker system by looking at the "container=lxc" environment + // variable. However, we run sysinit during image creation, at which + // point we're on a native system and this variable isn't set. + if fi, err := os.Stat("/home/veyron/init"); err == nil && fi.Mode().IsDir() { + return "docker" + } + if fi, err := os.Stat(upstartBin); err == nil { + if (fi.Mode() & 0100) != 0 { + return "upstart" + } + } + + if findSystemdSystemCtl() != "" { + return "systemd" + } + + return "" +} + +// New returns the appropriate implementation of InstallSystemInit for the +// underlying system. +func New(system string, sd *ServiceDescription) InstallSystemInit { + switch system { + case "docker": + return (*DockerService)(sd) + case "upstart": + return (*UpstartService)(sd) + case "systemd": + return (*SystemdService)(sd) + default: + return nil + } +} + +/////////////////////////////////////////////////////////////////////////////// +// Upstart support + +// See http://upstart.ubuntu.com/cookbook/ for info on upstart + +// UpstartService is the implementation of InstallSystemInit interfacing with +// the Upstart system. +type UpstartService ServiceDescription + +var upstartTemplate = `# This file was auto-generated by the Vanadium SysInit tool. +# Date: {{.Date}} +# +# {{.Service}} - {{.Description}} +# +# Upstart config for Ubuntu-GCE + +description "{{.Description}}" + +start on runlevel [2345] +stop on runlevel [!2345] +{{if .Environment}} +# Environment variables +{{range $var, $value := .Environment}} +env {{$var}}={{$value}}{{end}} +{{end}} +respawn +respawn limit 10 5 +umask 022 + +pre-start script + test -x {{.Binary}} || { stop; exit 0; } + mkdir -p -m0755 /var/log/veyron + chown -R {{.User}} /var/log/veyron +end script + +script + set -e + echo '{{.Service}} starting' + exec sudo -u {{.User}} {{range $cmd := .Command}} {{$cmd}}{{end}} +end script +` + +// Implements the InstallSystemInit method. +func (u *UpstartService) Install() error { + file := fmt.Sprintf("%s/%s.conf", upstartDir, u.Service) + return (*ServiceDescription)(u).writeTemplate(upstartTemplate, file) +} + +// Print implements the InstallSystemInit method. +func (u *UpstartService) Print() error { + return (*ServiceDescription)(u).writeTemplate(upstartTemplate, "") +} + +// Implements the InstallSystemInit method. +func (u *UpstartService) Uninstall() error { + // For now, ignore any errors returned by Stop, since Stop complains + // when there is no instance to stop. + // TODO(caprita): Only call Stop if there are running instances. + u.Stop() + file := fmt.Sprintf("%s/%s.conf", upstartDir, u.Service) + return os.Remove(file) +} + +// Start implements the InstallSystemInit method. +func (u *UpstartService) Start() error { + return action(upstartBin, "start", u.Service) +} + +// Stop implements the InstallSystemInit method. +func (u *UpstartService) Stop() error { + return action(upstartBin, "stop", u.Service) +} + +/////////////////////////////////////////////////////////////////////////////// +// Systemd support + +// SystemdService is the implementation of InstallSystemInit interfacing with +// the Systemd system. +type SystemdService ServiceDescription + +const systemdTemplate = `# This file was auto-generated by the Vanadium SysInit tool. +# Date: {{.Date}} +# +# {{.Service}} - {{.Description}} +# +[Unit] +Description={{.Description}} +After=openntpd.service + +[Service] +User={{.User}}{{if .Environment}}{{println}}Environment={{range $var, $value := .Environment}}"{{$var}}={{$value}}" {{end}}{{end}} +ExecStart={{range $cmd := .Command}}{{$cmd}} {{end}} +ExecReload=/bin/kill -HUP $MAINPID +KillMode=process +Restart=always +RestartSecs=10 +StandardOutput=syslog + + +[Install] +WantedBy=multi-user.target +` + +// Install implements the InstallSystemInit method. +func (s *SystemdService) Install() error { + file := fmt.Sprintf("%s/%s.service", systemdDir, s.Service) + if err := (*ServiceDescription)(s).writeTemplate(systemdTemplate, file); err != nil { + return fmt.Errorf("failed to write template (uid= %d, euid= %d): %v", os.Getuid(), os.Geteuid(), err) + } + file = fmt.Sprintf("%s/veyron.conf", systemdTmpFileDir) + f, err := os.Create(file) + if err != nil { + return err + } + f.WriteString("d /var/log/veyron 0755 veyron veyron\n") + f.Close() + err = action("systemd-tmpfiles", "--create", file) + if err != nil { + return err + } + + // First call disable to get rid of any symlink lingering around from a previous install + // We don't care about the return status on the disable action. + action(findSystemdSystemCtl(), "disable", s.Service) + return action(findSystemdSystemCtl(), "enable", s.Service) +} + +// Print implements the InstallSystemInit method. +func (s *SystemdService) Print() error { + return (*ServiceDescription)(s).writeTemplate(systemdTemplate, "") +} + +// Uninstall implements the InstallSystemInit method. +func (s *SystemdService) Uninstall() error { + if err := s.Stop(); err != nil { + return err + } + if err := action(findSystemdSystemCtl(), "disable", s.Service); err != nil { + return err + } + file := fmt.Sprintf("%s/%s.service", systemdDir, s.Service) + return os.Remove(file) +} + +// Start implements the InstallSystemInit method. +func (s *SystemdService) Start() error { + return action(findSystemdSystemCtl(), "start", s.Service) +} + +// Stop implements the InstallSystemInit method. +func (s *SystemdService) Stop() error { + return action(findSystemdSystemCtl(), "stop", s.Service) +} + +// This is a variable so it can be overridden for testing. +var findSystemdSystemCtl = func() string { + // Systems using systemd may have systemctl in one of several possible places. This finds it. + paths := []string{"/sbin", "/bin", "/usr/bin", "/usr/sbin"} + + for _, path := range paths { + testpath := filepath.Join(path, "systemctl") + if fi, err := os.Stat(testpath); err == nil && (fi.Mode()&0100) != 0 { + return testpath + } + } + + return "" +} + +/////////////////////////////////////////////////////////////////////////////// +// Docker support + +// DockerService is the implementation of InstallSystemInit interfacing with +// Docker. +type DockerService ServiceDescription + +const dockerTemplate = `#!/bin/bash +# This file was auto-generated by the Vanadium SysInit tool. +# Date: {{.Date}} +# +# {{.Service}} - {{.Description}} +# +set -e +{{if .Environment}} +# Environment variables +{{range $var, $value := .Environment}}export {{$var}}={{$value}}{{end}} +{{end}} +echo '{{.Service}} setup.' +test -x {{.Binary}} || { stop; exit 0; } +mkdir -p -m0755 /var/log/veyron +chown -R {{.User}} /var/log/veyron + +echo '{{.Service}} starting' +exec daemon -n {{.Service}} -r -A 2 -L 10 -M 5 -X '{{range $cmd := .Command}} {{$cmd}}{{end}}' & +` + +// Install implements the InstallSystemInit method. +func (s *DockerService) Install() error { + file := fmt.Sprintf("%s/%s.sh", dockerDir, s.Service) + if err := (*ServiceDescription)(s).writeTemplate(dockerTemplate, file); err != nil { + return err + } + os.Chmod(file, 0755) + return nil +} + +// Print implements the InstallSystemInit method. +func (s *DockerService) Print() error { + return (*ServiceDescription)(s).writeTemplate(dockerTemplate, "") +} + +// Uninstall implements the InstallSystemInit method. +func (s *DockerService) Uninstall() error { + if err := s.Stop(); err != nil { + return err + } + file := fmt.Sprintf("%s/%s.sh", dockerDir, s.Service) + return os.Remove(file) +} + +// Start implements the InstallSystemInit method. +func (s *DockerService) Start() error { + return action(fmt.Sprintf("%s/%s.sh", dockerDir, s.Service), "", s.Service) +} + +// Stop implements the InstallSystemInit method. +func (s *DockerService) Stop() error { + return action("daemon", fmt.Sprintf("-n %s --stop", s.Service), s.Service) +} diff --git a/x/ref/services/device/internal/sysinit/linux_test.go b/x/ref/services/device/internal/sysinit/linux_test.go new file mode 100644 index 000000000..06be016b3 --- /dev/null +++ b/x/ref/services/device/internal/sysinit/linux_test.go @@ -0,0 +1,129 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +// +build linux + +package sysinit + +import ( + "bufio" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestUpstart(t *testing.T) { + oldTemplate := upstartTemplate + upstartTemplate = `{{.Date}} +{{.Service}} +{{.Description}} +{{.Binary}} +{{.Command}} +` + oldUpstartDir := upstartDir + upstartDir, _ = ioutil.TempDir(".", "etc-init") + + defer func() { + upstartTemplate = oldTemplate + upstartDir = oldUpstartDir + }() + + defer os.RemoveAll(upstartDir) + u := &UpstartService{ + Service: "tester", + Description: "my test", + Binary: "/bin/echo", + Command: []string{"/bin/echo -n foo"}, + } + if err := u.Install(); err != nil { + t.Fatalf("unexpected error: %s", err) + } + rc, _ := os.Open(upstartDir + "/tester.conf") + lines := bufio.NewScanner(rc) + lines.Scan() + timestr := lines.Text() + _, err := time.Parse(dateFormat, timestr) + if err != nil { + t.Fatalf("unexpected error parsing time: %v, err: %v", t, err) + } + lines.Scan() + if lines.Text() != "tester" { + t.Fatalf("unexpected output: %s", lines.Text()) + } + lines.Scan() + lines.Scan() + if lines.Text() != "/bin/echo" { + t.Fatalf("unexpected output: %s", lines.Text()) + } + lines.Scan() + if lines.Scan() { + t.Fatalf("failed to find end of file") + } +} + +func TestSystemd(t *testing.T) { + s := &SystemdService{ + Service: "tester", + Description: "my test", + Binary: "/bin/echo", + Command: []string{"/bin/echo", "-n", "foo"}, + } + + oldSystemdDir := systemdDir + oldSystemdTmpFileDir := systemdTmpFileDir + oldAction := action + oldFindSystemdSystemCtl := findSystemdSystemCtl + + systemdDir, _ = ioutil.TempDir(".", "usr-lib-systemd-system") + defer os.RemoveAll(systemdDir) + systemdTmpFileDir, _ = ioutil.TempDir(".", "usr-lib-tmpfiles.d") + defer os.RemoveAll(systemdTmpFileDir) + + var cmd, act, srv string + action = func(command, action, service string) error { + cmd, act, srv = command, action, service + return nil + } + + findSystemdSystemCtl = func() string { + return "systemctl" + } + + defer func() { + systemdDir = oldSystemdDir + systemdTmpFileDir = oldSystemdTmpFileDir + action = oldAction + findSystemdSystemCtl = oldFindSystemdSystemCtl + }() + + if err := s.Install(); err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if want, got := "systemctl", cmd; want != got { + t.Errorf("action command: want %q, got %q", want, got) + } + if want, got := "enable", act; want != got { + t.Errorf("action action: want %q, got %q", want, got) + } + if want, got := "tester", srv; want != got { + t.Errorf("action service: want %q, got %q", want, got) + } + + c, err := ioutil.ReadFile(filepath.Join(systemdDir, "tester.service")) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + contents := string(c) + if !strings.Contains(contents, "Description=my test") { + t.Errorf("bad Description in generated service spec: %v", contents) + } + if !strings.Contains(contents, "ExecStart=/bin/echo -n foo") { + t.Errorf("bad ExecStart in generated service spec: %v", contents) + } +} diff --git a/x/ref/services/device/internal/sysinit/service_description.go b/x/ref/services/device/internal/sysinit/service_description.go new file mode 100644 index 000000000..42a7f2772 --- /dev/null +++ b/x/ref/services/device/internal/sysinit/service_description.go @@ -0,0 +1,85 @@ +// Copyright 2015 The Vanadium 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 sysinit + +import ( + "encoding/json" + "io/ioutil" + "os" + "text/template" + "time" + + "v.io/v23/verror" +) + +const pkgPath = "v.io/x/ref/services/device/internal/sysinit" + +var ( + errMarshalFailed = verror.Register(pkgPath+".errMarshalFailed", verror.NoRetry, "{1:}{2:} Marshal({3}) failed{:_}") + errWriteFileFailed = verror.Register(pkgPath+".errWriteFileFailed", verror.NoRetry, "{1:}{2:} WriteFile({3}) failed{:_}") + errReadFileFailed = verror.Register(pkgPath+".errReadFileFailed", verror.NoRetry, "{1:}{2:} ReadFile({3}) failed{:_}") + errUnmarshalFailed = verror.Register(pkgPath+".errUnmarshalFailed", verror.NoRetry, "{1:}{2:} Unmarshal({3}) failed{:_}") +) + +const dateFormat = "Jan 2 2006 at 15:04:05 (MST)" + +// ServiceDescription is a generic service description that represents the +// common configuration details for specific systems. +type ServiceDescription struct { + Service string // The name of the Service + Description string // A description of the Service + Environment map[string]string // Environment variables needed by the service + Binary string // The binary to be run + Command []string // The script/binary and command line options to use to start/stop the binary + User string // The username this service is to run as +} + +// TODO(caprita): Unit test. + +// SaveTo serializes the service description object to a file. +func (sd *ServiceDescription) SaveTo(fName string) error { + jsonSD, err := json.Marshal(sd) + if err != nil { + return verror.New(errMarshalFailed, nil, sd, err) + } + if err := ioutil.WriteFile(fName, jsonSD, 0600); err != nil { + return verror.New(errWriteFileFailed, nil, fName, err) + } + return nil +} + +// LoadFrom de-serializes the service description object from a file created by +// SaveTo. +func (sd *ServiceDescription) LoadFrom(fName string) error { + if sdBytes, err := ioutil.ReadFile(fName); err != nil { + return verror.New(errReadFileFailed, nil, fName, err) + } else if err := json.Unmarshal(sdBytes, sd); err != nil { + return verror.New(errUnmarshalFailed, nil, sdBytes, err) + } + return nil +} + +func (sd *ServiceDescription) writeTemplate(templateContents, file string) error { + conf, err := template.New(sd.Service + ".template").Parse(templateContents) + if err != nil { + return err + } + w := os.Stdout + if len(file) > 0 { + w, err = os.Create(file) + if err != nil { + return err + } + } + type tmp struct { + *ServiceDescription + Date string + } + data := &tmp{ + ServiceDescription: sd, + Date: time.Now().Format(dateFormat), + } + return conf.Execute(w, &data) +} diff --git a/x/ref/services/device/internal/sysinit/sysinit.go b/x/ref/services/device/internal/sysinit/sysinit.go new file mode 100644 index 000000000..67c433514 --- /dev/null +++ b/x/ref/services/device/internal/sysinit/sysinit.go @@ -0,0 +1,17 @@ +// Copyright 2015 The Vanadium 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 sysinit provides config generation for a variety of platforms and +// "init" systems such as upstart, systemd etc. It is intended purely for +// bootstrapping into the Vanadium system proper. +package sysinit + +// InstallSystemInit defines the interface that all configs must implement. +type InstallSystemInit interface { + Print() error + Install() error + Uninstall() error + Start() error + Stop() error +} diff --git a/x/ref/services/device/mgmt_v23_test.go b/x/ref/services/device/mgmt_v23_test.go new file mode 100644 index 000000000..6795679ab --- /dev/null +++ b/x/ref/services/device/mgmt_v23_test.go @@ -0,0 +1,646 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Test the device manager and related services and tools. +// +// By default, this script tests the device manager in a fashion amenable +// to automatic testing: the --single_user is passed to the device +// manager so that all device manager components run as the same user and +// no user input (such as an agent pass phrase) is needed. +// +// This script can exercise the device manager in two different modes. It +// can be executed like so: +// +// jiri go test -v . --v23.tests +// +// This will exercise the device manager's single user mode where all +// processes run as the same invoking user. +// +// Alternatively, the device manager can be executed in multiple account +// mode by providing the --deviceuser <deviceuser> and --appuser +// <appuser> flags. In this case, the device manager will run as user +// <devicemgr> and the test will run applications as user <appuser>. If +// executed in this fashion, root permissions will be required to install +// and it may require configuring an agent passphrase. For example: +// +// jiri go test -v . --v23.tests --deviceuser devicemanager --appuser vana +// +// NB: the accounts provided as arguments to this test must already exist. +// Also, the --v23.tests.shell-on-fail flag is useful to enable debugging +// output. Note that this flag does not work for some shells. Set +// $SHELL in that case. + +package device_test + +import ( + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "math/rand" + "os" + "os/user" + "path/filepath" + "regexp" + "runtime" + "strings" + "sync" + "testing" + "time" + + "v.io/x/ref" + _ "v.io/x/ref/runtime/factories/generic" + "v.io/x/ref/services/internal/dirprinter" + "v.io/x/ref/test/testutil" + "v.io/x/ref/test/v23test" +) + +var ( + appUserFlag string + deviceUserFlag string + hostname string + errTimeout = errors.New("timeout") +) + +func init() { + flag.StringVar(&appUserFlag, "appuser", "", "launch apps as the specified user") + flag.StringVar(&deviceUserFlag, "deviceuser", "", "run the device manager as the specified user") + name, err := os.Hostname() + if err != nil { + panic(fmt.Sprintf("Hostname() failed: %v", err)) + } + hostname = name +} + +func TestV23DeviceManagerSingleUser(t *testing.T) { + v23test.SkipUnlessRunningIntegrationTests(t) + sh := v23test.NewShell(t, nil) + defer sh.Cleanup() + + u, err := user.Current() + if err != nil { + t.Fatalf("couldn't get the current user: %v", err) + } + testCore(t, sh, u.Username, "", false) +} + +func TestV23DeviceManagerMultiUser(t *testing.T) { + t.Skip("Permissions need to be configured properly on credentials.") + v23test.SkipUnlessRunningIntegrationTests(t) + sh := v23test.NewShell(t, nil) + defer sh.Cleanup() + + u, err := user.Current() + if err != nil { + t.Fatalf("couldn't get the current user: %v", err) + } + + if u.Username == "veyron" && runTestOnThisPlatform { + // We are running on the builder so run the multiuser + // test with default user names. These will be created as + // required. + makeTestAccounts(t, sh) + testCore(t, sh, "vana", "devicemanager", true) + return + } + + if len(deviceUserFlag) > 0 && len(appUserFlag) > 0 { + testCore(t, sh, appUserFlag, deviceUserFlag, true) + } else { + t.Logf("Test skipped because running in multiuser mode requires --appuser and --deviceuser flags") + } +} + +func testCore(t *testing.T, sh *v23test.Shell, appUser, deviceUser string, withSuid bool) { + defer fmt.Fprintf(os.Stderr, "--------------- SHUTDOWN ---------------\n") + + // Call sh.StartRootMountTable() first, since it updates sh.Vars, which is + // copied by various Cmds at Cmd creation time. + sh.StartRootMountTable() + + // When running with --with_suid, TMPDIR must grant the invoking user rwx + // permissions and world x permissions for all parent directories back to /. + // Otherwise, the with_suid user will not be able to use absolute paths. On + // Darwin, TMPDIR defaults to a directory hierararchy in /var that is 0700. + // This is unworkable, so force TMPDIR to /tmp in this case. + // + // In addition, even when running without --with_suid, on Darwin the default + // TMPDIR results in "socket path (...) exceeds maximum allowed socket path + // length" errors, so we always set TMPDIR to /tmp. + oldTmpdir := os.Getenv("TMPDIR") + os.Setenv("TMPDIR", "/tmp") + defer os.Setenv("TMPDIR", oldTmpdir) + + var ( + workDir = sh.MakeTempDir() + binStagingDir = mkSubdir(t, workDir, "bin") + dmInstallDir = filepath.Join(workDir, "dm") + + // Most vanadium command-line utilities will be run by a + // principal that has "root:u:alice" as its blessing. + // (Where "root" comes from i.Principal().BlessingStore().Default()). + // Create those credentials and options to use to setup the + // binaries with them. + aliceCreds = sh.ForkCredentials("u:alice") + + // Build all the command-line tools and set them up to run as alice. + // applicationd/binaryd servers will be run by alice too. + // TODO: applicationd/binaryd should run as a separate "service" role, as + // alice is just a user. + namespaceBin = sh.Cmd(v23test.BuildGoPkg(sh, "v.io/x/ref/cmd/namespace")).WithCredentials(aliceCreds) + deviceBin = sh.Cmd(v23test.BuildGoPkg(sh, "v.io/x/ref/services/device/device")).WithCredentials(aliceCreds) + binarydBin = sh.Cmd(v23test.BuildGoPkg(sh, "v.io/x/ref/services/binary/binaryd")).WithCredentials(aliceCreds) + applicationdBin = sh.Cmd(v23test.BuildGoPkg(sh, "v.io/x/ref/services/application/applicationd")).WithCredentials(aliceCreds) + + // The devicex script is not provided with any credentials, it + // will generate its own. This means that on "devicex start" + // the device will have no useful credentials and until "device + // claim" is invoked (as alice), it will just sit around + // waiting to be claimed. + // + // Other binaries, like applicationd and binaryd will be run by alice. + deviceScript = sh.Cmd("./devicex") + + mtName = "devices/" + hostname // Name under which the device manager will publish itself. + ) + + defer func() { + if !t.Failed() { + return + } + fmt.Fprintf(os.Stderr, "--------------- START DUMP %s ---------------\n", workDir) + if err := dirprinter.DumpDir(os.Stderr, workDir); err != nil { + fmt.Fprintf(os.Stderr, "Failed: %v\n", err) + } + fmt.Fprintf(os.Stderr, "--------------- END DUMP %s ---------------\n", workDir) + }() + deviceScript.Vars["V23_DEVICE_DIR"] = dmInstallDir + // Make sure the devicex command is not provided with credentials. Note, this + // is analogous to what's done in + // v.io/x/ref/services/device/deviced/internal/impl/utiltest.RunDeviceManager. + delete(deviceScript.Vars, ref.EnvCredentials) + delete(deviceScript.Vars, ref.EnvAgentPath) + + // We also need some tools running with different sets of credentials... + + // Administration tasks will be performed with a blessing that represents a corporate + // adminstrator (which is usually a role account) + adminCreds := sh.ForkCredentials("r:admin") + adminDeviceBin := deviceBin.WithCredentials(adminCreds) + debugBin := sh.Cmd(v23test.BuildGoPkg(sh, "v.io/x/ref/services/debug/debug")).WithCredentials(adminCreds) + + // A special set of credentials will be used to give two blessings to the device manager + // when claiming it -- one blessing will be from the corporate administrator role who owns + // the machine, and the other will be a manufacturer blessing. (This is a hack until + // there's a way to separately supply a manufacturer blessing. Eventually, the claim + // would really be done by the administator, and the adminstrator's blessing would get + // added to the manufacturer's blessing, which would already be present.) + claimCreds := sh.ForkCredentials("r:admin", "m:orange:zphone5:ime-i007") + claimDeviceBin := deviceBin.WithCredentials(claimCreds) + + // Another set of credentials be used to represent the application publisher, who + // signs and pushes binaries + pubCreds := sh.ForkCredentials("a:rovio") + pubDeviceBin := deviceBin.WithCredentials(pubCreds) + applicationBin := sh.Cmd(v23test.BuildGoPkg(sh, "v.io/x/ref/services/application/application")).WithCredentials(pubCreds) + binaryBin := sh.Cmd(v23test.BuildGoPkg(sh, "v.io/x/ref/services/binary/binary")).WithCredentials(pubCreds) + + if withSuid { + // In multiuser mode, deviceUserFlag needs execute access to + // tempDir. + if err := os.Chmod(workDir, 0711); err != nil { + t.Fatalf("os.Chmod() failed: %v", err) + } + } + + buildAndCopyBinaries( + t, sh, binStagingDir, + "v.io/x/ref/services/device/deviced", + "v.io/x/ref/services/device/restarter", + "v.io/x/ref/services/agent/v23agentd", + "v.io/x/ref/services/device/suidhelper", + "v.io/x/ref/services/device/inithelper") + + appDName := "applications" + devicedAppName := filepath.Join(appDName, "deviced", "test") + + deviceScriptArguments := []string{ + "install", + binStagingDir, + } + + if withSuid { + deviceScriptArguments = append(deviceScriptArguments, "--devuser="+deviceUser) + } else { + deviceScriptArguments = append(deviceScriptArguments, "--single_user") + } + + deviceScriptArguments = append(deviceScriptArguments, []string{ + "--origin=" + devicedAppName, + "--", + "--v23.tcp.address=127.0.0.1:0", + "--neighborhood-name=" + fmt.Sprintf("%s-%d-%d", hostname, os.Getpid(), rand.Int()), + }...) + + withArgs(deviceScript, deviceScriptArguments...).Run() + withArgs(deviceScript, "start").Run() + dmLog := filepath.Join(dmInstallDir, "dmroot/device-manager/logs/deviced.INFO") + stopDevMgr := func() { + withArgs(deviceScript, "stop").Run() + if dmLogF, err := os.Open(dmLog); err != nil { + t.Errorf("Failed to read dm log: %v", err) + } else { + fmt.Fprintf(os.Stderr, "--------------- START DM LOG ---------------\n") + defer dmLogF.Close() + if _, err := io.Copy(os.Stderr, dmLogF); err != nil { + t.Errorf("Error dumping dm log: %v", err) + } + fmt.Fprintf(os.Stderr, "--------------- END DM LOG ---------------\n") + } + } + var stopDevMgrOnce sync.Once + defer stopDevMgrOnce.Do(stopDevMgr) + // Grab the endpoint for the claimable service from the device manager's + // log. + var claimableEP string + expiry := time.Now().Add(30 * time.Second) + for { + if time.Now().After(expiry) { + t.Fatalf("Timed out looking for claimable endpoint in %v", dmLog) + } + startLog, err := ioutil.ReadFile(dmLog) + if err != nil { + t.Logf("Couldn't read log %v: %v", dmLog, err) + time.Sleep(time.Second) + continue + } + re := regexp.MustCompile(`Unclaimed device manager endpoint: (.*)`) + matches := re.FindSubmatch(startLog) + if len(matches) == 0 { + t.Logf("Couldn't find match in %v [%s]", dmLog, startLog) + time.Sleep(time.Second) + continue + } + if len(matches) < 2 { + t.Fatalf("Wrong match in %v (%d) %v", dmLog, len(matches), string(matches[0])) + } + claimableEP = string(matches[len(matches)-1]) + break + } + // Claim the device as "root:u:alice:myworkstation". + claimCmd := withArgs(claimDeviceBin, "claim", claimableEP, "myworkstation") + // TODO(caprita): Sometimes, the claim RPC fails to complete, even + // though the server-side claiming seems to succeed. See + // https://github.com/vanadium/build/issues/58 + claimCmd.ExitErrorIsOk = true + claimCmd.Run() + + resolve := func(name string) string { + res := "" + if err := testutil.RetryFor(10*time.Second, func() error { + // Set ExitErrorIsOk to true since we expect "namespace resolve" to fail + // if the name doesn't exist. + c := withArgs(namespaceBin, "resolve", "-s", name) + c.ExitErrorIsOk = true + c.AddStderrWriter(os.Stderr) + if res = tr(c.Stdout()); len(res) > 0 { + return nil + } + return testutil.TryAgain(errors.New("resolve returned nothing")) + }); err != nil { + t.Fatal(err) + } + return res + } + + // Wait for the device manager to publish its mount table entry. + mtEP := resolve(mtName) + withArgs(adminDeviceBin, "acl", "set", mtName+"/devmgr/device", "root:u:alice", "Read,Resolve,Write").Run() + + if withSuid { + withArgs(adminDeviceBin, "associate", "add", mtName+"/devmgr/device", appUser, "root:u:alice").Run() + associations := withArgs(adminDeviceBin, "associate", "list", mtName+"/devmgr/device").Stdout() + if got, expected := strings.Trim(associations, "\n "), "root:u:alice "+appUser; got != expected { + t.Fatalf("association test, got %v, expected %v", got, expected) + } + } + + // Verify the device's default blessing is as expected. + mfrBlessing := "root:m:orange:zphone5:ime-i007:myworkstation" + ownerBlessing := "root:r:admin:myworkstation" + c := withArgs(debugBin, "stats", "read", mtName+"/devmgr/__debug/stats/security/principal/*/blessingstore/*") + c.Run() + c.S.ExpectSetEventuallyRE(".*Default Blessings[ ]+" + mfrBlessing + "," + ownerBlessing) + + // Get the device's profile, which should be set to non-empty string + c = withArgs(adminDeviceBin, "describe", mtName+"/devmgr/device") + c.Run() + parts := c.S.ExpectRE(`{Profiles:map\[(.*):{}\]}`, 1) + expectOneMatch := func(parts [][]string) string { + if len(parts) != 1 || len(parts[0]) != 2 { + t.Fatalf("%s: failed to match profile: %#v", caller(1), parts) + } + return parts[0][1] + } + deviceProfile := expectOneMatch(parts) + if len(deviceProfile) == 0 { + t.Fatalf("failed to get profile") + } + + // Start a binaryd server that will serve the binary for the test + // application to be installed on the device. + binarydName := "binaries" + withArgs(binarydBin, + "--name="+binarydName, + "--root-dir="+filepath.Join(workDir, "binstore"), + "--v23.tcp.address=127.0.0.1:0", + "--http=127.0.0.1:0").Start() + // Allow publishers to update binaries + withArgs(deviceBin, "acl", "set", binarydName, "root:a", "Write").Run() + + // We are also going to use the binaryd binary as our test app binary. Once our test app + // binary is published to the binaryd server started above, this (augmented with a + // timestamp) is the name the test app binary will have. + sampleAppBinName := binarydName + "/binaryd" + + // Start an applicationd server that will serve the application + // envelope for the test application to be installed on the device. + withArgs(applicationdBin, + "--name="+appDName, + "--store="+mkSubdir(t, workDir, "appstore"), + "--v23.tcp.address=127.0.0.1:0").Start() + // Allow publishers to create and update envelopes + withArgs(deviceBin, "acl", "set", appDName, "root:a", "Read,Write,Resolve").Run() + + sampleAppName := appDName + "/testapp" + appPubName := "testbinaryd" + appEnvelopeFilename := filepath.Join(workDir, "app.envelope") + appEnvelope := fmt.Sprintf("{\"Title\":\"BINARYD\", \"Args\":[\"--name=%s\", \"--root-dir=./binstore\", \"--v23.tcp.address=127.0.0.1:0\", \"--http=127.0.0.1:0\"], \"Binary\":{\"File\":%q}, \"Env\":[ \"%s=1\", \"PATH=%s\"]}", appPubName, sampleAppBinName, ref.EnvCredentialsNoAgent, os.Getenv("PATH")) + ioutil.WriteFile(appEnvelopeFilename, []byte(appEnvelope), 0666) + defer os.Remove(appEnvelopeFilename) + + output := withArgs(applicationBin, "put", sampleAppName+"/0", deviceProfile, appEnvelopeFilename).Stdout() + if got, want := tr(output), fmt.Sprintf("Application envelope added for profile %s.", deviceProfile); got != want { + t.Fatalf("got %q, want %q", got, want) + } + + // Verify that the envelope we uploaded shows up with glob. + c = withArgs(applicationBin, "match", sampleAppName, deviceProfile) + c.Run() + parts = c.S.ExpectSetEventuallyRE(`"Title": "(.*)",`, `"File": "(.*)",`) + if got, want := len(parts), 2; got != want { + t.Fatalf("got %d, want %d", got, want) + } + for line, want := range []string{"BINARYD", sampleAppBinName} { + if got := parts[line][1]; got != want { + t.Fatalf("got %q, want %q", got, want) + } + } + + // Publish the app (This uses the binarydBin binary and the testapp envelope from above) + withArgs(pubDeviceBin, "publish", "-from", filepath.Dir(binarydBin.Path), "-readers", "root:r:admin", filepath.Base(binarydBin.Path)+":testapp").Run() + if got := withArgs(namespaceBin, "glob", sampleAppBinName).Stdout(); len(got) == 0 { + t.Fatalf("glob failed for %q", sampleAppBinName) + } + + // Install the app on the device. + c = withArgs(deviceBin, "install", mtName+"/devmgr/apps", sampleAppName) + c.Run() + installationName := c.S.ReadLine() + if installationName == "" { + t.Fatalf("got empty installation name from install") + } + + // Verify that the installation shows up when globbing the device manager. + output = withArgs(namespaceBin, "glob", mtName+"/devmgr/apps/BINARYD/*").Stdout() + if got, want := tr(output), installationName; got != want { + t.Fatalf("got %q, want %q", got, want) + } + + // Start an instance of the app, granting it blessing extension myapp. + c = withArgs(deviceBin, "instantiate", installationName, "myapp") + c.Run() + instanceName := c.S.ReadLine() + if instanceName == "" { + t.Fatalf("got empty instance name from new") + } + withArgs(deviceBin, "run", instanceName).Run() + + resolve(mtName + "/" + appPubName) + + // Verify that the instance shows up when globbing the device manager. + output = withArgs(namespaceBin, "glob", mtName+"/devmgr/apps/BINARYD/*/*").Stdout() + if got, want := tr(output), instanceName; got != want { + t.Fatalf("got %q, want %q", got, want) + } + + c = withArgs(debugBin, "stats", "read", instanceName+"/stats/system/pid") + c.Run() + pid := c.S.ExpectRE("[0-9]+$", 1)[0][0] + uname, err := getUserForPid(sh, pid) + if err != nil { + t.Errorf("getUserForPid could not determine the user running pid %v", pid) + } else if uname != appUser { + t.Errorf("app expected to be running as %v but is running as %v", appUser, uname) + } + + // Verify the app's blessings. We check the default blessing, as well as the + // "..." blessing, which should be the default blessing plus a publisher blessing. + userBlessing := "root:u:alice:myapp" + pubBlessing := "root:a:rovio:apps:published:binaryd" + appBlessing := mfrBlessing + ":a:" + pubBlessing + "," + ownerBlessing + ":a:" + pubBlessing + c = withArgs(debugBin, "stats", "read", instanceName+"/stats/security/principal/*/blessingstore/*") + c.Run() + c.S.ExpectSetEventuallyRE(".*Default Blessings[ ]+"+userBlessing+"$", "[.][.][.][ ]+"+userBlessing+","+appBlessing) + + // Kill and delete the instance. + withArgs(deviceBin, "kill", instanceName).Run() + withArgs(deviceBin, "delete", instanceName).Run() + + // Verify that logs, but not stats, show up when globbing the + // not-running instance. + if output = withArgs(namespaceBin, "glob", instanceName+"/stats/...").Stdout(); len(output) > 0 { + t.Fatalf("no output expected for glob %s/stats/..., got %q", output, instanceName) + } + if output = withArgs(namespaceBin, "glob", instanceName+"/logs/...").Stdout(); len(output) == 0 { + t.Fatalf("output expected for glob %s/logs/..., but got none", instanceName) + } + + // TODO: The deviced binary should probably be published by someone other than rovio :-) + // Maybe publishing the deviced binary should eventually use "device publish" too? + // For now, it uses the "application" and "binary" tools directly to ensure that those work + + // Upload a deviced binary + devicedAppBinName := binarydName + "/deviced" + withArgs(binaryBin, "upload", devicedAppBinName, v23test.BuildGoPkg(sh, "v.io/x/ref/services/device/deviced")).Run() + // Allow root:r:admin and its devices to read the binary + withArgs(deviceBin, "acl", "set", devicedAppBinName, "root:r:admin", "Read").Run() + + // Upload a device manager envelope. + devicedEnvelopeFilename := filepath.Join(workDir, "deviced.envelope") + devicedEnvelope := fmt.Sprintf("{\"Title\":\"device manager\", \"Binary\":{\"File\":%q}, \"Env\":[ \"%s=1\", \"PATH=%s\"]}", devicedAppBinName, ref.EnvCredentialsNoAgent, os.Getenv("PATH")) + ioutil.WriteFile(devicedEnvelopeFilename, []byte(devicedEnvelope), 0666) + defer os.Remove(devicedEnvelopeFilename) + withArgs(applicationBin, "put", devicedAppName, deviceProfile, devicedEnvelopeFilename).Run() + // Allow root:r:admin and its devices to read the envelope + withArgs(deviceBin, "acl", "set", devicedAppName, "root:r:admin", "Read").Run() + + // Update the device manager. + withArgs(adminDeviceBin, "update", mtName+"/devmgr/device").Run() + resolveChange := func(name, old string) string { + res := "" + if err := testutil.RetryFor(10*time.Second, func() error { + // Set ExitErrorIsOk to true since we expect "namespace resolve" to fail + // if the name doesn't exist. + c := withArgs(namespaceBin, "resolve", "-s", name) + c.ExitErrorIsOk = true + c.AddStderrWriter(os.Stderr) + switch res = tr(c.Stdout()); { + case res == "": + return testutil.TryAgain(errors.New("resolve returned nothing")) + case res == old: + return testutil.TryAgain(errors.New("no change")) + } + return nil + }); err != nil { + t.Fatal(err) + } + return res + } + mtEP = resolveChange(mtName, mtEP) + + // Verify that device manager's mounttable is still published under the + // expected name (hostname). + if withArgs(namespaceBin, "glob", mtName).Stdout() == "" { + t.Fatalf("failed to glob %s", mtName) + } + + // Revert the device manager + // The argument to "device revert" is a glob pattern. So we need to + // wait for devmgr to be mounted before running the command. + resolve(mtEP + "/devmgr") + withArgs(adminDeviceBin, "revert", mtName+"/devmgr/device").Run() + mtEP = resolveChange(mtName, mtEP) + + // Verify that device manager's mounttable is still published under the + // expected name (hostname). + if withArgs(namespaceBin, "glob", mtName).Stdout() == "" { + t.Fatalf("failed to glob %s", mtName) + } + + // Verify that the local mounttable exists, and that the device manager, + // the global namespace, and the neighborhood are mounted on it. + resolve(mtEP + "/devmgr") + resolve(mtEP + "/nh") + resolve(mtEP + "/global") + + namespaceRoot := sh.Vars[ref.EnvNamespacePrefix] + output = withArgs(namespaceBin, "resolve", "-s", mtEP+"/global").Stdout() + if got, want := tr(output), namespaceRoot; got != want { + t.Fatalf("got %q, want %q", got, want) + } + + // Kill the device manager (which causes it to be restarted), wait for + // the endpoint to change. + withArgs(deviceBin, "kill", mtName+"/devmgr/device").Run() + mtEP = resolveChange(mtName, mtEP) + + // Shut down the device manager. + stopDevMgrOnce.Do(stopDevMgr) + + // Wait for the mounttable entry to go away. + resolveGone := func(name string) string { + res := "" + if err := testutil.RetryFor(10*time.Second, func() error { + // Set ExitErrorIsOk to true since we expect "namespace resolve" to fail + // if the name doesn't exist. + c := withArgs(namespaceBin, "resolve", "-s", name) + c.ExitErrorIsOk = true + c.AddStderrWriter(os.Stderr) + if res = tr(c.Stdout()); len(res) == 0 { + return nil + } + return testutil.TryAgain(errors.New("mount table entry still exists")) + }); err != nil { + t.Fatal(err) + } + return res + } + resolveGone(mtName) + + var fi []os.FileInfo + + // This doesn't work in multiuser mode because dmInstallDir is + // owned by the device manager user and unreadable by the user + // running this test. + if !withSuid { + fi, err = ioutil.ReadDir(dmInstallDir) + if err != nil { + t.Fatalf("failed to readdir for %q: %v", dmInstallDir, err) + } + } + + withArgs(deviceScript, "uninstall").Run() + + fi, err = ioutil.ReadDir(dmInstallDir) + if err == nil || len(fi) > 0 { + t.Fatalf("managed to read %d entries from %q", len(fi), dmInstallDir) + } + if err != nil && !strings.Contains(err.Error(), "no such file or directory") { + t.Fatalf("wrong error: %v", err) + } +} + +func withArgs(cmd *v23test.Cmd, args ...string) *v23test.Cmd { + res := cmd.Clone() + res.Args = append(res.Args, args...) + return res +} + +func buildAndCopyBinaries(t *testing.T, sh *v23test.Shell, destinationDir string, packages ...string) { + var args []string + for _, pkg := range packages { + args = append(args, v23test.BuildGoPkg(sh, pkg)) + } + args = append(args, destinationDir) + sh.Cmd("/bin/cp", args...).Run() +} + +func mkSubdir(t *testing.T, parent, child string) string { + dir := filepath.Join(parent, child) + if err := os.Mkdir(dir, 0755); err != nil { + t.Fatalf("failed to create %q: %v", dir, err) + } + return dir +} + +var re = regexp.MustCompile("[ \t]+") + +// getUserForPid determines the username running process pid. +func getUserForPid(sh *v23test.Shell, pid string) (string, error) { + pidString := sh.Cmd("/bin/ps", psFlags).Stdout() + for _, line := range strings.Split(pidString, "\n") { + fields := re.Split(line, -1) + if len(fields) > 1 && pid == fields[1] { + return fields[0], nil + } + } + return "", fmt.Errorf("Couldn't determine the user for pid %s", pid) +} + +// tr trims off trailing newline characters. +func tr(s string) string { + return strings.TrimRight(s, "\n") +} + +// caller returns a string of the form <filename>:<lineno>. +func caller(skip int) string { + _, file, line, _ := runtime.Caller(skip + 1) + return fmt.Sprintf("%s:%d", filepath.Base(file), line) +} + +func TestMain(m *testing.M) { + v23test.TestMain(m) +} diff --git a/x/ref/services/device/restarter/doc.go b/x/ref/services/device/restarter/doc.go new file mode 100644 index 000000000..20d7ea969 --- /dev/null +++ b/x/ref/services/device/restarter/doc.go @@ -0,0 +1,38 @@ +// Copyright 2018 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated via go generate. +// DO NOT UPDATE MANUALLY + +/* +Command restarter runs a child command and optionally restarts it depending on +the setting of the --restart-exit-code flag. + +Example: + # Prints "foo" just once. + $ restarter echo foo + # Prints "foo" in a loop. + $ restarter --restart-exit-code=13 bash -c "echo foo; sleep 1; exit 13" + # Prints "foo" just once. + $ restarter --restart-exit-code=\!13 bash -c "echo foo; sleep 1; exit 13" + +Usage: + restarter [flags] command [command_args...] + +The command is started as a subprocess with the given [command_args...]. + +The restarter flags are: + -restart-exit-code= + If non-empty, will restart the command when it exits, provided that the + command's exit code matches the value of this flag. The value must be an + integer, or an integer preceded by '!' (in which case all exit codes except + the flag will trigger a restart). + +The global flags are: + -metadata=<just specify -metadata to activate> + Displays metadata for the program and exits. + -time=false + Dump timing information to stderr before exiting the program. +*/ +package main diff --git a/x/ref/services/device/restarter/main.go b/x/ref/services/device/restarter/main.go new file mode 100644 index 000000000..62f95922a --- /dev/null +++ b/x/ref/services/device/restarter/main.go @@ -0,0 +1,133 @@ +// Copyright 2016 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The following enables go generate to generate the doc.go file. +//go:generate gendoc . -help + +package main + +import ( + "flag" + "fmt" + "os/exec" + "strconv" + "syscall" + + "v.io/v23/verror" + "v.io/x/lib/cmdline" + vsignals "v.io/x/ref/lib/signals" +) + +const pkgPath = "v.io/x/ref/services/device/restarter" + +var ( + errCantParseRestartExitCode = verror.Register(pkgPath+".errCantParseRestartExitCode", verror.NoRetry, "{1:}{2:} Failed to parse restart exit code{:_}") + + restartExitCode string +) + +func main() { + cmdRestarter.Flags.StringVar(&restartExitCode, "restart-exit-code", "", "If non-empty, will restart the command when it exits, provided that the command's exit code matches the value of this flag. The value must be an integer, or an integer preceded by '!' (in which case all exit codes except the flag will trigger a restart).") + + cmdline.HideGlobalFlagsExcept() + cmdline.Main(cmdRestarter) +} + +var cmdRestarter = &cmdline.Command{ + Runner: cmdline.RunnerFunc(runRestarter), + Name: "restarter", + Short: "Runs a child command and restarts it depending on its exit code", + Long: ` +Command restarter runs a child command and optionally restarts it depending on the setting of the --restart-exit-code flag. + +Example: + # Prints "foo" just once. + $ restarter echo foo + # Prints "foo" in a loop. + $ restarter --restart-exit-code=13 bash -c "echo foo; sleep 1; exit 13" + # Prints "foo" just once. + $ restarter --restart-exit-code=\!13 bash -c "echo foo; sleep 1; exit 13" +`, + ArgsName: "command [command_args...]", + ArgsLong: ` +The command is started as a subprocess with the given [command_args...]. +`, +} + +func runRestarter(env *cmdline.Env, args []string) error { + var restartOpts restartOptions + if err := restartOpts.parse(); err != nil { + return env.UsageErrorf("%v", err) + } + + if len(args) == 0 { + return env.UsageErrorf("command not specified") + } + + exitCode := 0 + for { + // Run the client and wait for it to finish. + cmd := exec.Command(flag.Args()[0], flag.Args()[1:]...) + cmd.Stdin = env.Stdin + cmd.Stdout = env.Stdout + cmd.Stderr = env.Stderr + + if err := cmd.Start(); err != nil { + return fmt.Errorf("Error starting child: %v", err) + } + shutdown := make(chan struct{}) + go func() { + // TODO(caprita): Revisit why we're only sending down + // the first signal we get to the child (instead of + // sending all signals we can handle). + select { + case sig := <-vsignals.ShutdownOnSignals(nil): + // TODO(caprita): Should we also relay double + // signal to the child? That currently just + // force exits the current process. + if sig == vsignals.STOP { + sig = syscall.SIGTERM + } + cmd.Process.Signal(sig) + case <-shutdown: + } + }() + cmd.Wait() + close(shutdown) + exitCode = cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() + if !restartOpts.restart(exitCode) { + break + } + } + if exitCode != 0 { + return cmdline.ErrExitCode(exitCode) + } + return nil +} + +type restartOptions struct { + enabled, unless bool + code int +} + +func (opts *restartOptions) parse() error { + code := restartExitCode + if code == "" { + return nil + } + opts.enabled = true + if code[0] == '!' { + opts.unless = true + code = code[1:] + } + var err error + if opts.code, err = strconv.Atoi(code); err != nil { + return verror.New(errCantParseRestartExitCode, nil, err) + } + return nil +} + +func (opts *restartOptions) restart(exitCode int) bool { + return opts.enabled && opts.unless != (exitCode == opts.code) +} diff --git a/x/ref/services/device/restarter/v23_test.go b/x/ref/services/device/restarter/v23_test.go new file mode 100644 index 000000000..f7d210fd6 --- /dev/null +++ b/x/ref/services/device/restarter/v23_test.go @@ -0,0 +1,112 @@ +// Copyright 2016 The Vanadium 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 main_test + +import ( + "bytes" + "io/ioutil" + "path/filepath" + "testing" + "text/template" + + _ "v.io/x/ref/runtime/factories/roaming" + "v.io/x/ref/test/v23test" +) + +func TestV23RestartExitCode(t *testing.T) { + v23test.SkipUnlessRunningIntegrationTests(t) + sh := v23test.NewShell(t, nil) + defer sh.Cleanup() + var ( + scriptDir = sh.MakeTempDir() + counter = filepath.Join(scriptDir, "counter") + script = filepath.Join(scriptDir, "test.sh") + restarter = v23test.BuildGoPkg(sh, "v.io/x/ref/services/device/restarter") + ) + if err := writeScript( + script, + `#!/bin/bash +# Increment the contents of the counter file +readonly COUNT=$(expr $(<"{{.Counter}}") + 1) +echo -n $COUNT >{{.Counter}} +# Exit code is 0 if the counter is less than 5 +[[ $COUNT -lt 5 ]]; exit $? +`, + struct{ Counter string }{ + Counter: counter, + }); err != nil { + t.Fatal(err) + } + + tests := []struct { + RestartExitCode string + WantError string + WantCounter string + }{ + { + // With --restart-exit-code=0, the script should be kicked off + // 5 times till the counter reaches 5 and the last iteration of + // the script will exit. + RestartExitCode: "0", + WantError: "exit status 1", + WantCounter: "5", + }, + { + // With --restart-exit-code=!0, the script will be executed only once. + RestartExitCode: "!0", + WantError: "", + WantCounter: "1", + }, + { + // --restart-exit-code=!1, should be the same + // as --restart-exit-code=0 for this + // particular script only exits with 0 or 1 + RestartExitCode: "!1", + WantError: "exit status 1", + WantCounter: "5", + }, + } + for _, test := range tests { + // Clear out the counter file. + if err := ioutil.WriteFile(counter, []byte("0"), 0644); err != nil { + t.Fatalf("%q: %v", counter, err) + } + // Run the script under the restarter. + cmd := sh.Cmd(restarter, "--restart-exit-code="+test.RestartExitCode, "bash", "-c", script) + cmd.ExitErrorIsOk = true + cmd.Run() + var gotError string + if cmd.Err != nil { + gotError = cmd.Err.Error() + } + if got, want := gotError, test.WantError; got != want { + t.Errorf("%+v: Got %q, want %q", test, got, want) + } + if buf, err := ioutil.ReadFile(counter); err != nil { + t.Errorf("ioutil.ReadFile(%q) failed: %v", counter, err) + } else if got, want := string(buf), test.WantCounter; got != want { + t.Errorf("%+v: Got %q, want %q", test, got, want) + } + } +} + +func writeScript(dstfile, tmpl string, args interface{}) error { + t, err := template.New(dstfile).Parse(tmpl) + if err != nil { + return err + } + var buf bytes.Buffer + if err := t.Execute(&buf, args); err != nil { + return err + } + if err := ioutil.WriteFile(dstfile, buf.Bytes(), 0700); err != nil { + return err + } + return nil +} + +func TestMain(m *testing.M) { + v23test.TestMain(m) +} diff --git a/x/ref/services/device/suidhelper/main.go b/x/ref/services/device/suidhelper/main.go new file mode 100644 index 000000000..bde662544 --- /dev/null +++ b/x/ref/services/device/suidhelper/main.go @@ -0,0 +1,29 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Command suidhelper runs the provided command as the specified user identity. +// It should be installed setuid root. +package main + +// suidhelper deliberately attempts to be as simple as possible to +// simplify reviewing it for security concerns. + +import ( + "flag" + "fmt" + "os" + + "v.io/x/ref/services/device/internal/suid" +) + +func main() { + flag.Parse() + fmt.Fprintln(os.Stderr, os.Args) + if err := suid.Run(os.Environ()); err != nil { + fmt.Fprintln(os.Stderr, "Failed with:", err) + // TODO(rjkroege): We should really only print the usage message + // if the error is related to interpreting flags. + flag.Usage() + } +} diff --git a/x/ref/services/device/util_darwin_test.go b/x/ref/services/device/util_darwin_test.go new file mode 100644 index 000000000..7538f8747 --- /dev/null +++ b/x/ref/services/device/util_darwin_test.go @@ -0,0 +1,90 @@ +// Copyright 2015 The Vanadium 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 device_test + +import ( + "fmt" + "os/user" + "strconv" + "strings" + "testing" + + "v.io/x/ref/test/v23test" +) + +const runTestOnThisPlatform = true +const psFlags = "-ej" + +type uidMap map[int]struct{} + +func (uids uidMap) findAvailable() (int, error) { + // Accounts starting at 501 are available. Don't use the largest + // UID because on a corporate imaged Mac, this will overlap with + // another employee's UID. Instead, use the first available UID >= 501. + for newuid := 501; newuid < 1e6; newuid++ { + if _, ok := uids[newuid]; !ok { + uids[newuid] = struct{}{} + return newuid, nil + } + } + return 0, fmt.Errorf("Couldn't find an available UID") +} + +func newUidMap(sh *v23test.Shell) uidMap { + // `dscl . -list /Users UniqueID` into a datastructure. + userstring := sh.Cmd("dscl", ".", "-list", "/Users", "UniqueID").Stdout() + users := strings.Split(userstring, "\n") + + uids := make(map[int]struct{}, len(users)) + for _, line := range users { + fields := re.Split(line, -1) + if len(fields) > 1 { + if uid, err := strconv.Atoi(fields[1]); err == nil { + uids[uid] = struct{}{} + } + } + } + return uids +} + +func makeAccount(sh *v23test.Shell, uid int, uname, fullname string) { + sudo := "/usr/bin/sudo" + args := []string{"dscl", ".", "-create", "/Users/" + uname} + + run := func(extraArgs ...string) { + sh.Cmd(sudo, append(args, extraArgs...)...).Run() + } + + run() + run("UserShell", "/bin/bash") + run("RealName", fullname) + run("UniqueID", strconv.FormatInt(int64(uid), 10)) + run("PrimaryGroupID", "20") +} + +func makeTestAccounts(t *testing.T, sh *v23test.Shell) { + _, needVanaErr := user.Lookup("vana") + _, needDevErr := user.Lookup("devicemanager") + + if needVanaErr == nil && needDevErr == nil { + return + } + + uids := newUidMap(sh) + if needVanaErr != nil { + vanauid, err := uids.findAvailable() + if err != nil { + t.Fatalf("Can't make test accounts: %v", err) + } + makeAccount(sh, vanauid, "vana", "Vanadium White") + } + if needDevErr != nil { + devmgruid, err := uids.findAvailable() + if err != nil { + t.Fatalf("Can't make test accounts: %v", err) + } + makeAccount(sh, devmgruid, "devicemanager", "Devicemanager") + } +} diff --git a/x/ref/services/device/util_linux_test.go b/x/ref/services/device/util_linux_test.go new file mode 100644 index 000000000..c8af32475 --- /dev/null +++ b/x/ref/services/device/util_linux_test.go @@ -0,0 +1,28 @@ +// Copyright 2015 The Vanadium 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 device_test + +import ( + "os/user" + "testing" + + "v.io/x/ref/test/v23test" +) + +const psFlags = "-eouser:20,pid" + +func makeTestAccounts(t *testing.T, sh *v23test.Shell) { + sudo := "/usr/bin/sudo" + + if _, err := user.Lookup("vana"); err != nil { + sh.Cmd(sudo, "/usr/sbin/adduser", "--no-create-home", "vana").Run() + } + + if _, err := user.Lookup("devicemanager"); err != nil { + sh.Cmd(sudo, "/usr/sbin/adduser", "--no-create-home", "devicemanager").Run() + } +} + +const runTestOnThisPlatform = true diff --git a/x/ref/services/internal/binarylib/client.go b/x/ref/services/internal/binarylib/client.go new file mode 100644 index 000000000..8bc789bd4 --- /dev/null +++ b/x/ref/services/internal/binarylib/client.go @@ -0,0 +1,363 @@ +// Copyright 2015 The Vanadium 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 binarylib + +// TODO(jsimsa): Implement parallel download and upload. + +import ( + "bytes" + "crypto/md5" + "crypto/sha256" + "encoding/hex" + "fmt" + "hash" + "io" + "io/ioutil" + "os" + "path/filepath" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/security" + "v.io/v23/services/binary" + "v.io/v23/services/repository" + "v.io/v23/verror" + "v.io/x/ref/services/internal/packages" +) + +var ( + errOperationFailed = verror.Register(pkgPath+".errOperationFailed", verror.NoRetry, "{1:}{2:} operation failed{:_}") +) + +const ( + nAttempts = 2 + partSize = 1 << 22 + subpartSize = 1 << 12 +) + +func Delete(ctx *context.T, name string) error { + if err := repository.BinaryClient(name).Delete(ctx); err != nil { + ctx.Errorf("Delete() failed: %v", err) + return err + } + return nil +} + +type indexedPart struct { + part binary.PartInfo + index int + offset int64 +} + +func downloadPartAttempt(ctx *context.T, w io.WriteSeeker, client repository.BinaryClientStub, ip *indexedPart) bool { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + if _, err := w.Seek(ip.offset, 0); err != nil { + ctx.Errorf("Seek(%v, 0) failed: %v", ip.offset, err) + return false + } + stream, err := client.Download(ctx, int32(ip.index)) + if err != nil { + ctx.Errorf("Download(%v) failed: %v", ip.index, err) + return false + } + h, nreceived := md5.New(), 0 + rStream := stream.RecvStream() + for rStream.Advance() { + bytes := rStream.Value() + if _, err := w.Write(bytes); err != nil { + ctx.Errorf("Write() failed: %v", err) + return false + } + h.Write(bytes) + nreceived += len(bytes) + } + + if err := rStream.Err(); err != nil { + ctx.Errorf("Advance() failed: %v", err) + return false + } + if err := stream.Finish(); err != nil { + ctx.Errorf("Finish() failed: %v", err) + return false + } + if expected, got := ip.part.Checksum, hex.EncodeToString(h.Sum(nil)); expected != got { + ctx.Errorf("Unexpected checksum: expected %v, got %v", expected, got) + return false + } + if expected, got := ip.part.Size, int64(nreceived); expected != got { + ctx.Errorf("Unexpected size: expected %v, got %v", expected, got) + return false + } + return true +} + +func downloadPart(ctx *context.T, w io.WriteSeeker, client repository.BinaryClientStub, ip *indexedPart) bool { + for i := 0; i < nAttempts; i++ { + if downloadPartAttempt(ctx, w, client, ip) { + return true + } + } + return false +} + +func Stat(ctx *context.T, name string) (repository.MediaInfo, error) { + client := repository.BinaryClient(name) + _, mediaInfo, err := client.Stat(ctx) + if err != nil { + return repository.MediaInfo{}, err + } + return mediaInfo, nil +} + +func download(ctx *context.T, w io.WriteSeeker, von string) (repository.MediaInfo, error) { + client := repository.BinaryClient(von) + parts, mediaInfo, err := client.Stat(ctx) + if err != nil { + ctx.Errorf("Stat() failed: %v", err) + return repository.MediaInfo{}, err + } + for _, part := range parts { + if part.Checksum == binary.MissingChecksum { + return repository.MediaInfo{}, verror.New(verror.ErrNoExist, ctx) + } + } + offset := int64(0) + for i, part := range parts { + ip := &indexedPart{part, i, offset} + if !downloadPart(ctx, w, client, ip) { + return repository.MediaInfo{}, verror.New(errOperationFailed, ctx) + } + offset += part.Size + } + return mediaInfo, nil +} + +func Download(ctx *context.T, von string) ([]byte, repository.MediaInfo, error) { + dir, prefix := "", "" + file, err := ioutil.TempFile(dir, prefix) + if err != nil { + ctx.Errorf("TempFile(%v, %v) failed: %v", dir, prefix, err) + return nil, repository.MediaInfo{}, verror.New(errOperationFailed, ctx) + } + defer os.Remove(file.Name()) + defer file.Close() + mediaInfo, err := download(ctx, file, von) + if err != nil { + return nil, repository.MediaInfo{}, verror.New(errOperationFailed, ctx) + } + bytes, err := ioutil.ReadFile(file.Name()) + if err != nil { + ctx.Errorf("ReadFile(%v) failed: %v", file.Name(), err) + return nil, repository.MediaInfo{}, verror.New(errOperationFailed, ctx) + } + return bytes, mediaInfo, nil +} + +func DownloadToFile(ctx *context.T, von, path string) error { + dir := filepath.Dir(path) + prefix := fmt.Sprintf(".download.%s.", filepath.Base(path)) + file, err := ioutil.TempFile(dir, prefix) + if err != nil { + ctx.Errorf("TempFile(%v, %v) failed: %v", dir, prefix, err) + return verror.New(errOperationFailed, ctx) + } + defer file.Close() + mediaInfo, err := download(ctx, file, von) + if err != nil { + if err := os.Remove(file.Name()); err != nil { + ctx.Errorf("Remove(%v) failed: %v", file.Name(), err) + } + return verror.New(errOperationFailed, ctx) + } + perm := os.FileMode(0600) + if err := file.Chmod(perm); err != nil { + ctx.Errorf("Chmod(%v) failed: %v", perm, err) + if err := os.Remove(file.Name()); err != nil { + ctx.Errorf("Remove(%v) failed: %v", file.Name(), err) + } + return verror.New(errOperationFailed, ctx) + } + if err := os.Rename(file.Name(), path); err != nil { + ctx.Errorf("Rename(%v, %v) failed: %v", file.Name(), path, err) + if err := os.Remove(file.Name()); err != nil { + ctx.Errorf("Remove(%v) failed: %v", file.Name(), err) + } + return verror.New(errOperationFailed, ctx) + } + if err := packages.SaveMediaInfo(path, mediaInfo); err != nil { + ctx.Errorf("packages.SaveMediaInfo(%v, %v) failed: %v", path, mediaInfo, err) + if err := os.Remove(path); err != nil { + ctx.Errorf("Remove(%v) failed: %v", path, err) + } + return verror.New(errOperationFailed, ctx) + } + return nil +} + +func DownloadUrl(ctx *context.T, von string) (string, int64, error) { + url, ttl, err := repository.BinaryClient(von).DownloadUrl(ctx) + if err != nil { + ctx.Errorf("DownloadUrl() failed: %v", err) + return "", 0, err + } + return url, ttl, nil +} + +func uploadPartAttempt(ctx *context.T, h hash.Hash, r io.ReadSeeker, client repository.BinaryClientStub, part int, size int64) (bool, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + offset := int64(part * partSize) + if _, err := r.Seek(offset, 0); err != nil { + ctx.Errorf("Seek(%v, 0) failed: %v", offset, err) + return false, nil + } + stream, err := client.Upload(ctx, int32(part)) + if err != nil { + ctx.Errorf("Upload(%v) failed: %v", part, err) + return false, nil + } + bufferSize := partSize + if remaining := size - offset; remaining < int64(bufferSize) { + bufferSize = int(remaining) + } + buffer := make([]byte, bufferSize) + + nread := 0 + for nread < len(buffer) { + n, err := r.Read(buffer[nread:]) + nread += n + if err != nil && (err != io.EOF || nread < len(buffer)) { + ctx.Errorf("Read() failed: %v", err) + return false, nil + } + } + sender := stream.SendStream() + for from := 0; from < len(buffer); from += subpartSize { + to := from + subpartSize + if to > len(buffer) { + to = len(buffer) + } + if err := sender.Send(buffer[from:to]); err != nil { + ctx.Errorf("Send() failed: %v", err) + return false, nil + } + } + // TODO(gauthamt): To detect corruption, the upload checksum needs + // to be computed here rather than on the binary server. + if err := sender.Close(); err != nil { + ctx.Errorf("Close() failed: %v", err) + parts, _, statErr := client.Stat(ctx) + if statErr != nil { + ctx.Errorf("Stat() failed: %v", statErr) + if deleteErr := client.Delete(ctx); err != nil { + ctx.Errorf("Delete() failed: %v", deleteErr) + } + return false, err + } + if parts[part].Checksum == binary.MissingChecksum { + return false, nil + } + } + if err := stream.Finish(); err != nil { + ctx.Errorf("Finish() failed: %v", err) + parts, _, statErr := client.Stat(ctx) + if statErr != nil { + ctx.Errorf("Stat() failed: %v", statErr) + if deleteErr := client.Delete(ctx); err != nil { + ctx.Errorf("Delete() failed: %v", deleteErr) + } + return false, err + } + if parts[part].Checksum == binary.MissingChecksum { + return false, nil + } + } + h.Write(buffer) + return true, nil +} + +func uploadPart(ctx *context.T, h hash.Hash, r io.ReadSeeker, client repository.BinaryClientStub, part int, size int64) error { + for i := 0; i < nAttempts; i++ { + if success, err := uploadPartAttempt(ctx, h, r, client, part, size); success || err != nil { + return err + } + } + return verror.New(errOperationFailed, ctx) +} + +func upload(ctx *context.T, r io.ReadSeeker, mediaInfo repository.MediaInfo, von string) (*security.Signature, error) { + client := repository.BinaryClient(von) + offset, whence := int64(0), 2 + size, err := r.Seek(offset, whence) + if err != nil { + ctx.Errorf("Seek(%v, %v) failed: %v", offset, whence, err) + return nil, verror.New(errOperationFailed, ctx) + } + nparts := (size-1)/partSize + 1 + if err := client.Create(ctx, int32(nparts), mediaInfo); err != nil { + ctx.Errorf("Create() failed: %v", err) + return nil, err + } + h := sha256.New() + for i := 0; int64(i) < nparts; i++ { + if err := uploadPart(ctx, h, r, client, i, size); err != nil { + return nil, err + } + } + return signHash(ctx, h) +} + +func signHash(ctx *context.T, h hash.Hash) (*security.Signature, error) { + hash := h.Sum(nil) + sig, err := v23.GetPrincipal(ctx).Sign(hash[:]) + if err != nil { + ctx.Errorf("Sign() of hash failed:%v", err) + return nil, err + } + return &sig, nil +} + +func Upload(ctx *context.T, von string, data []byte, mediaInfo repository.MediaInfo) (*security.Signature, error) { + buffer := bytes.NewReader(data) + return upload(ctx, buffer, mediaInfo, von) +} + +func Sign(ctx *context.T, in io.Reader) (*security.Signature, error) { + out := sha256.New() + if _, err := io.Copy(out, in); err != nil { + return nil, err + } + return signHash(ctx, out) +} + +func UploadFromFile(ctx *context.T, von, path string) (*security.Signature, error) { + file, err := os.Open(path) + if err != nil { + ctx.Errorf("Open(%v) failed: %v", path, err) + return nil, verror.New(errOperationFailed, ctx) + } + defer file.Close() + mediaInfo, err := packages.LoadMediaInfo(path) + if err != nil { + mediaInfo = packages.MediaInfoForFileName(path) + } + return upload(ctx, file, mediaInfo, von) +} + +func UploadFromDir(ctx *context.T, von, sourceDir string) (*security.Signature, error) { + dir, err := ioutil.TempDir("", "create-package-") + if err != nil { + return nil, err + } + defer os.RemoveAll(dir) + zipfile := filepath.Join(dir, "file.zip") + if err := packages.CreateZip(zipfile, sourceDir); err != nil { + return nil, err + } + return UploadFromFile(ctx, von, zipfile) +} diff --git a/x/ref/services/internal/binarylib/client_test.go b/x/ref/services/internal/binarylib/client_test.go new file mode 100644 index 000000000..89729a513 --- /dev/null +++ b/x/ref/services/internal/binarylib/client_test.go @@ -0,0 +1,192 @@ +// Copyright 2015 The Vanadium 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 binarylib + +import ( + "bytes" + "crypto/sha256" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/services/repository" + "v.io/x/ref/services/internal/packages" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" +) + +const ( + v23Prefix = "vanadium_binary_repository" +) + +func setupRepository(t *testing.T, ctx *context.T) (string, func()) { + // Setup the root of the binary repository. + rootDir, err := ioutil.TempDir("", v23Prefix) + if err != nil { + t.Fatalf("TempDir() failed: %v", err) + } + path, perm := filepath.Join(rootDir, VersionFile), os.FileMode(0600) + if err := ioutil.WriteFile(path, []byte(Version), perm); err != nil { + ctx.Fatalf("WriteFile(%v, %v, %v) failed: %v", path, Version, perm, err) + } + // Setup and start the binary repository server. + depth := 2 + state, err := NewState(rootDir, "http://test-root-url", depth) + if err != nil { + t.Fatalf("NewState(%v, %v) failed: %v", rootDir, depth, err) + } + dispatcher, err := NewDispatcher(ctx, state) + if err != nil { + t.Fatalf("NewDispatcher() failed: %v\n", err) + } + ctx, cancel := context.WithCancel(ctx) + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", dispatcher) + if err != nil { + t.Fatalf("NewServer() failed: %v", err) + } + von := naming.JoinAddressName(server.Status().Endpoints[0].String(), "test") + return von, func() { + if err := os.Remove(path); err != nil { + t.Fatalf("Remove(%v) failed: %v", path, err) + } + // Check that any directories and files that were created to + // represent the binary objects have been garbage collected. + if err := os.RemoveAll(rootDir); err != nil { + t.Fatalf("Remove(%v) failed: %v", rootDir, err) + } + cancel() + <-server.Closed() + } +} + +// TestBufferAPI tests the binary repository client-side library +// interface using buffers. +func TestBufferAPI(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + rg := testutil.NewRandGenerator(t.Logf) + + von, cleanup := setupRepository(t, ctx) + defer cleanup() + data := rg.RandomBytes(rg.RandomIntn(10 << 20)) + mediaInfo := repository.MediaInfo{Type: "application/octet-stream"} + sig, err := Upload(ctx, von, data, mediaInfo) + if err != nil { + t.Fatalf("Upload(%v) failed: %v", von, err) + } + p := v23.GetPrincipal(ctx) + if sig != nil { + // verify the principal signature + h := sha256.Sum256(data) + if !sig.Verify(p.PublicKey(), h[:]) { + t.Fatalf("Failed to verify upload signature(%v)", sig) + } + // verify that Sign called directly also produces a working signature + reader := bytes.NewReader(data) + sig2, err := Sign(ctx, reader) + if err != nil { + t.Fatalf("Sign failed: %v", err) + } + if !sig2.Verify(p.PublicKey(), h[:]) { + t.Fatalf("Failed to verify signature from Sign(%v, %v)", sig, sig2) + } + } else { + t.Fatalf("Upload(%v) failed to generate principal(%v) signature", von, p) + } + output, outInfo, err := Download(ctx, von) + if err != nil { + t.Fatalf("Download(%v) failed: %v", von, err) + } + if bytes.Compare(data, output) != 0 { + t.Errorf("Data mismatch:\nexpected %v %v\ngot %v %v", len(data), data[:100], len(output), output[:100]) + } + if err := Delete(ctx, von); err != nil { + t.Errorf("Delete(%v) failed: %v", von, err) + } + if _, _, err := Download(ctx, von); err == nil { + t.Errorf("Download(%v) did not fail", von) + } + if !reflect.DeepEqual(mediaInfo, outInfo) { + t.Errorf("unexpected media info: expected %v, got %v", mediaInfo, outInfo) + } +} + +// TestFileAPI tests the binary repository client-side library +// interface using files. +func TestFileAPI(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + rg := testutil.NewRandGenerator(t.Logf) + + von, cleanup := setupRepository(t, ctx) + defer cleanup() + // Create up to 10MB of random bytes. + data := rg.RandomBytes(rg.RandomIntn(10 << 20)) + dir, prefix := "", "" + src, err := ioutil.TempFile(dir, prefix) + if err != nil { + t.Fatalf("TempFile(%v, %v) failed: %v", dir, prefix, err) + } + defer os.Remove(src.Name()) + defer src.Close() + dstdir, err := ioutil.TempDir(dir, prefix) + if err != nil { + t.Fatalf("TempDir(%v, %v) failed: %v", dir, prefix, err) + } + defer os.RemoveAll(dstdir) + dst, err := ioutil.TempFile(dstdir, prefix) + if err != nil { + t.Fatalf("TempFile(%v, %v) failed: %v", dstdir, prefix, err) + } + defer dst.Close() + if _, err := src.Write(data); err != nil { + t.Fatalf("Write() failed: %v", err) + } + if _, err := UploadFromFile(ctx, von, src.Name()); err != nil { + t.Fatalf("UploadFromFile(%v, %v) failed: %v", von, src.Name(), err) + } + if err := DownloadToFile(ctx, von, dst.Name()); err != nil { + t.Fatalf("DownloadToFile(%v, %v) failed: %v", von, dst.Name(), err) + } + output, err := ioutil.ReadFile(dst.Name()) + if err != nil { + t.Errorf("ReadFile(%v) failed: %v", dst.Name(), err) + } + if bytes.Compare(data, output) != 0 { + t.Errorf("Data mismatch:\nexpected %v %v\ngot %v %v", len(data), data[:100], len(output), output[:100]) + } + jMediaInfo, err := ioutil.ReadFile(packages.MediaInfoFile(dst.Name())) + if err != nil { + t.Errorf("ReadFile(%v) failed: %v", packages.MediaInfoFile(dst.Name()), err) + } + if expected := `{"Type":"application/octet-stream","Encoding":""}`; string(jMediaInfo) != expected { + t.Errorf("unexpected media info: expected %q, got %q", expected, string(jMediaInfo)) + } + if err := Delete(ctx, von); err != nil { + t.Errorf("Delete(%v) failed: %v", von, err) + } +} + +// TestDownloadUrl tests the binary repository client-side library +// DownloadUrl method. +func TestDownloadUrl(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + von, cleanup := setupRepository(t, ctx) + defer cleanup() + url, _, err := DownloadUrl(ctx, von) + if err != nil { + t.Fatalf("DownloadUrl(%v) failed: %v", von, err) + } + if got, want := url, "http://test-root-url/test"; got != want { + t.Fatalf("unexpect output: got %v, want %v", got, want) + } +} diff --git a/x/ref/services/internal/binarylib/dispatcher.go b/x/ref/services/internal/binarylib/dispatcher.go new file mode 100644 index 000000000..4c6d60189 --- /dev/null +++ b/x/ref/services/internal/binarylib/dispatcher.go @@ -0,0 +1,63 @@ +// Copyright 2015 The Vanadium 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 binarylib + +import ( + "path/filepath" + + "v.io/v23/context" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/services/repository" + "v.io/x/ref/services/internal/pathperms" +) + +const ( + VersionFile = "VERSION" + Version = "1.0" +) + +// dispatcher holds the state of the binary repository dispatcher. +type dispatcher struct { + state *state + permsStore *pathperms.PathStore +} + +// NewDispatcher is the dispatcher factory. +func NewDispatcher(ctx *context.T, state *state) (rpc.Dispatcher, error) { + return &dispatcher{ + state: state, + permsStore: pathperms.NewPathStore(ctx), + }, nil +} + +// DISPATCHER INTERFACE IMPLEMENTATION + +func permsPath(rootDir, suffix string) string { + var dir string + if suffix == "" { + // Directory is in namespace overlapped with Vanadium namespace + // so hide it. + dir = filepath.Join(rootDir, "__acls") + } else { + dir = filepath.Join(rootDir, suffix, "acls") + } + return dir +} + +func newAuthorizer(rootDir, suffix string, permsStore *pathperms.PathStore) (security.Authorizer, error) { + return pathperms.NewHierarchicalAuthorizer( + permsPath(rootDir, ""), + permsPath(rootDir, suffix), + permsStore) +} + +func (d *dispatcher) Lookup(_ *context.T, suffix string) (interface{}, security.Authorizer, error) { + auth, err := newAuthorizer(d.state.rootDir, suffix, d.permsStore) + if err != nil { + return nil, nil, err + } + return repository.BinaryServer(newBinaryService(d.state, suffix, d.permsStore)), auth, nil +} diff --git a/x/ref/services/internal/binarylib/fs_utils.go b/x/ref/services/internal/binarylib/fs_utils.go new file mode 100644 index 000000000..6545a6bda --- /dev/null +++ b/x/ref/services/internal/binarylib/fs_utils.go @@ -0,0 +1,150 @@ +// Copyright 2015 The Vanadium 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 binarylib + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" + + "v.io/v23/context" + "v.io/v23/verror" +) + +const ( + checksumFileName = "checksum" + dataFileName = "data" + lockFileName = "lock" + nameFileName = "name" + mediaInfoFileName = "mediainfo" +) + +// checksumExists checks whether the given part path is valid and +// contains a checksum. The implementation uses the existence of +// the path dir to determine whether the part is valid, and the +// existence of checksum to determine whether the binary part +// exists. +func checksumExists(ctx *context.T, path string) error { + switch _, err := os.Stat(path); { + case os.IsNotExist(err): + return verror.New(ErrInvalidPart, nil, path) + case err != nil: + ctx.Errorf("Stat(%v) failed: %v", path, err) + return verror.New(ErrOperationFailed, nil, path) + } + checksumFile := filepath.Join(path, checksumFileName) + _, err := os.Stat(checksumFile) + switch { + case os.IsNotExist(err): + return verror.New(verror.ErrNoExist, nil, path) + case err != nil: + ctx.Errorf("Stat(%v) failed: %v", checksumFile, err) + return verror.New(ErrOperationFailed, nil, path) + default: + return nil + } +} + +// generatePartPath generates a path for the given binary part. +func (i *binaryService) generatePartPath(part int) string { + return generatePartPath(i.path, part) +} + +func generatePartPath(dir string, part int) string { + return filepath.Join(dir, fmt.Sprintf("%d", part)) +} + +// getParts returns a collection of paths to the parts of the binary. +func getParts(ctx *context.T, path string) ([]string, error) { + infos, err := ioutil.ReadDir(path) + if err != nil { + ctx.Errorf("ReadDir(%v) failed: %v", path, err) + return []string{}, verror.New(ErrOperationFailed, nil, path) + } + nDirs := 0 + for _, info := range infos { + if info.IsDir() { + nDirs++ + } + } + result := make([]string, nDirs) + for _, info := range infos { + if info.IsDir() { + partName := info.Name() + idx, err := strconv.Atoi(partName) + if err != nil { + ctx.Errorf("Atoi(%v) failed: %v", partName, err) + return []string{}, verror.New(ErrOperationFailed, nil, path) + } + if idx < 0 || idx >= len(infos) || result[idx] != "" { + return []string{}, verror.New(ErrOperationFailed, nil, path) + } + result[idx] = filepath.Join(path, partName) + } else { + if info.Name() == nameFileName || info.Name() == mediaInfoFileName { + continue + } + // The only entries should correspond to the part dirs. + return []string{}, verror.New(ErrOperationFailed, nil, path) + } + } + return result, nil +} + +// createObjectNameTree returns a tree of all the valid object names in the +// repository. +func (i *binaryService) createObjectNameTree() *treeNode { + pattern := i.state.rootDir + for d := 0; d < i.state.depth; d++ { + pattern = filepath.Join(pattern, "*") + } + pattern = filepath.Join(pattern, "*", nameFileName) + matches, err := filepath.Glob(pattern) + if err != nil { + return nil + } + tree := newTreeNode() + for _, m := range matches { + name, err := ioutil.ReadFile(m) + if err != nil { + continue + } + elems := strings.Split(string(name), string(filepath.Separator)) + tree.find(elems, true) + } + return tree +} + +type treeNode struct { + children map[string]*treeNode +} + +func newTreeNode() *treeNode { + return &treeNode{children: make(map[string]*treeNode)} +} + +func (n *treeNode) find(names []string, create bool) *treeNode { + for { + if len(names) == 0 { + return n + } + if next, ok := n.children[names[0]]; ok { + n = next + names = names[1:] + continue + } + if create { + nn := newTreeNode() + n.children[names[0]] = nn + n = nn + names = names[1:] + continue + } + return nil + } +} diff --git a/x/ref/services/internal/binarylib/http.go b/x/ref/services/internal/binarylib/http.go new file mode 100644 index 000000000..a7e10d827 --- /dev/null +++ b/x/ref/services/internal/binarylib/http.go @@ -0,0 +1,54 @@ +// Copyright 2015 The Vanadium 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 binarylib + +import ( + "net/http" + "os" + "path/filepath" + "strings" + + "v.io/v23/context" + "v.io/v23/verror" + "v.io/x/ref/services/internal/multipart" +) + +// NewHTTPRoot returns an implementation of http.FileSystem that can be used +// to serve the content in the binary service. +func NewHTTPRoot(ctx *context.T, state *state) http.FileSystem { + return &httpRoot{ctx: ctx, state: state} +} + +type httpRoot struct { + ctx *context.T + state *state +} + +// TODO(caprita): Tie this in with DownloadUrl, to control which binaries +// are downloadable via url. + +// Open implements http.FileSystem. It uses the multipart file implementation +// to wrap the content parts into one logical file. +func (r httpRoot) Open(name string) (http.File, error) { + name = strings.TrimPrefix(name, "/") + r.ctx.Infof("HTTP handler opening %s", name) + parts, err := getParts(r.ctx, r.state.dir(name)) + if err != nil { + return nil, err + } + partFiles := make([]*os.File, len(parts)) + for i, part := range parts { + if err := checksumExists(r.ctx, part); err != nil { + return nil, err + } + dataPath := filepath.Join(part, dataFileName) + var err error + if partFiles[i], err = os.Open(dataPath); err != nil { + r.ctx.Errorf("Open(%v) failed: %v", dataPath, err) + return nil, verror.New(ErrOperationFailed, nil, dataPath) + } + } + return multipart.NewFile(name, partFiles) +} diff --git a/x/ref/services/internal/binarylib/http_test.go b/x/ref/services/internal/binarylib/http_test.go new file mode 100644 index 000000000..198cb758b --- /dev/null +++ b/x/ref/services/internal/binarylib/http_test.go @@ -0,0 +1,86 @@ +// Copyright 2015 The Vanadium 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 binarylib_test + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "io/ioutil" + "net/http" + "testing" + + "v.io/v23/services/repository" + + "v.io/x/ref/services/internal/binarylib" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" +) + +// TestHTTP checks that HTTP download works. +func TestHTTP(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + rg := testutil.NewRandGenerator(t.Logf) + + // TODO(caprita): This is based on TestMultiPart (impl_test.go). Share + // the code where possible. + for length := 2; length < 5; length++ { + binary, _, url, cleanup := startServer(t, ctx, 2) + defer cleanup() + // Create <length> chunks of up to 4MB of random bytes. + data := make([][]byte, length) + for i := 0; i < length; i++ { + // Random size, but at least 1 (avoid empty parts). + size := rg.RandomIntn(1000*binarylib.BufferLength) + 1 + data[i] = rg.RandomBytes(size) + } + mediaInfo := repository.MediaInfo{Type: "application/octet-stream"} + if err := binary.Create(ctx, int32(length), mediaInfo); err != nil { + t.Fatalf("Create() failed: %v", err) + } + for i := 0; i < length; i++ { + if streamErr, err := invokeUpload(t, ctx, binary, data[i], int32(i)); streamErr != nil || err != nil { + t.FailNow() + } + } + parts, _, err := binary.Stat(ctx) + if err != nil { + t.Fatalf("Stat() failed: %v", err) + } + response, err := http.Get(url) + if err != nil { + t.Fatal(err) + } + downloaded, err := ioutil.ReadAll(response.Body) + if err != nil { + t.Fatal(err) + } + from, to := 0, 0 + for i := 0; i < length; i++ { + hpart := md5.New() + to += len(data[i]) + if ld := len(downloaded); to > ld { + t.Fatalf("Download falls short: len(downloaded):%d, need:%d (i:%d, length:%d)", ld, to, i, length) + } + output := downloaded[from:to] + from = to + if bytes.Compare(output, data[i]) != 0 { + t.Fatalf("Unexpected output: expected %v, got %v", data[i], output) + } + hpart.Write(data[i]) + checksum := hex.EncodeToString(hpart.Sum(nil)) + if expected, got := checksum, parts[i].Checksum; expected != got { + t.Fatalf("Unexpected checksum: expected %v, got %v", expected, got) + } + if expected, got := len(data[i]), int(parts[i].Size); expected != got { + t.Fatalf("Unexpected size: expected %v, got %v", expected, got) + } + } + if err := binary.Delete(ctx); err != nil { + t.Fatalf("Delete() failed: %v", err) + } + } +} diff --git a/x/ref/services/internal/binarylib/impl_test.go b/x/ref/services/internal/binarylib/impl_test.go new file mode 100644 index 000000000..580474784 --- /dev/null +++ b/x/ref/services/internal/binarylib/impl_test.go @@ -0,0 +1,330 @@ +// Copyright 2015 The Vanadium 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 binarylib_test + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "fmt" + "net" + "net/http" + "reflect" + "testing" + + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/services/repository" + "v.io/v23/verror" + + "v.io/v23" + _ "v.io/x/ref/runtime/factories/roaming" + "v.io/x/ref/services/internal/binarylib" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" +) + +const ( + v23Prefix = "vanadium_binary_repository" +) + +// startServer starts the binary repository server. +func startServer(t *testing.T, ctx *context.T, depth int) (repository.BinaryClientMethods, string, string, func()) { + // Setup the root of the binary repository. + rootDir, cleanup := servicetest.SetupRootDir(t, "bindir") + prepDirectory(t, rootDir) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + state, err := binarylib.NewState(rootDir, listener.Addr().String(), depth) + if err != nil { + t.Fatalf("NewState(%v, %v, %v) failed: %v", rootDir, listener.Addr().String(), depth, err) + } + go func() { + if err := http.Serve(listener, http.FileServer(binarylib.NewHTTPRoot(ctx, state))); err != nil { + ctx.Fatalf("Serve() failed: %v", err) + } + }() + + // Setup and start the binary repository server. + dispatcher, err := binarylib.NewDispatcher(ctx, state) + if err != nil { + t.Fatalf("NewDispatcher failed: %v", err) + } + dontPublishName := "" + ctx, cancel := context.WithCancel(ctx) + _, server, err := v23.WithNewDispatchingServer(ctx, dontPublishName, dispatcher) + if err != nil { + t.Fatalf("NewServer(%q) failed: %v", dontPublishName, err) + } + endpoint := server.Status().Endpoints[0].String() + name := naming.JoinAddressName(endpoint, "test") + binary := repository.BinaryClient(name) + return binary, endpoint, fmt.Sprintf("http://%s/test", listener.Addr()), func() { + // Shutdown the binary repository server. + cancel() + <-server.Closed() + cleanup() + } +} + +// TestHierarchy checks that the binary repository works correctly for +// all possible valid values of the depth used for the directory +// hierarchy that stores binary objects in the local file system. +func TestHierarchy(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + rg := testutil.NewRandGenerator(t.Logf) + + for i := 0; i < md5.Size; i++ { + binary, ep, _, cleanup := startServer(t, ctx, i) + defer cleanup() + data := testData(rg) + + // Test the binary repository interface. + if err := binary.Create(ctx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil { + t.Fatalf("Create() failed: %v", err) + } + if streamErr, err := invokeUpload(t, ctx, binary, data, 0); streamErr != nil || err != nil { + t.FailNow() + } + parts, _, err := binary.Stat(ctx) + if err != nil { + t.Fatalf("Stat() failed: %v", err) + } + h := md5.New() + h.Write(data) + checksum := hex.EncodeToString(h.Sum(nil)) + if expected, got := checksum, parts[0].Checksum; expected != got { + t.Fatalf("Unexpected checksum: expected %v, got %v", expected, got) + } + if expected, got := len(data), int(parts[0].Size); expected != got { + t.Fatalf("Unexpected size: expected %v, got %v", expected, got) + } + output, streamErr, err := invokeDownload(t, ctx, binary, 0) + if streamErr != nil || err != nil { + t.FailNow() + } + if bytes.Compare(output, data) != 0 { + t.Fatalf("Unexpected output: expected %v, got %v", data, output) + } + results, _, err := testutil.GlobName(ctx, naming.JoinAddressName(ep, ""), "...") + if err != nil { + t.Fatalf("GlobName failed: %v", err) + } + if expected := []string{"", "test"}; !reflect.DeepEqual(results, expected) { + t.Errorf("Unexpected results: expected %q, got %q", expected, results) + } + if err := binary.Delete(ctx); err != nil { + t.Fatalf("Delete() failed: %v", err) + } + } +} + +// TestMultiPart checks that the binary repository supports multi-part +// uploads and downloads ranging the number of parts the test binary +// consists of. +func TestMultiPart(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + rg := testutil.NewRandGenerator(t.Logf) + + for length := 2; length < 5; length++ { + binary, _, _, cleanup := startServer(t, ctx, 2) + defer cleanup() + // Create <length> chunks of up to 4MB of random bytes. + data := make([][]byte, length) + for i := 0; i < length; i++ { + data[i] = testData(rg) + } + // Test the binary repository interface. + if err := binary.Create(ctx, int32(length), repository.MediaInfo{Type: "application/octet-stream"}); err != nil { + t.Fatalf("Create() failed: %v", err) + } + for i := 0; i < length; i++ { + if streamErr, err := invokeUpload(t, ctx, binary, data[i], int32(i)); streamErr != nil || err != nil { + t.FailNow() + } + } + parts, _, err := binary.Stat(ctx) + if err != nil { + t.Fatalf("Stat() failed: %v", err) + } + for i := 0; i < length; i++ { + hpart := md5.New() + output, streamErr, err := invokeDownload(t, ctx, binary, int32(i)) + if streamErr != nil || err != nil { + t.FailNow() + } + if bytes.Compare(output, data[i]) != 0 { + t.Fatalf("Unexpected output: expected %v, got %v", data[i], output) + } + hpart.Write(data[i]) + checksum := hex.EncodeToString(hpart.Sum(nil)) + if expected, got := checksum, parts[i].Checksum; expected != got { + t.Fatalf("Unexpected checksum: expected %v, got %v", expected, got) + } + if expected, got := len(data[i]), int(parts[i].Size); expected != got { + t.Fatalf("Unexpected size: expected %v, got %v", expected, got) + } + } + if err := binary.Delete(ctx); err != nil { + t.Fatalf("Delete() failed: %v", err) + } + } +} + +// TestResumption checks that the binary interface supports upload +// resumption ranging the number of parts the uploaded binary consists +// of. +func TestResumption(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + rg := testutil.NewRandGenerator(t.Logf) + + for length := 2; length < 5; length++ { + binary, _, _, cleanup := startServer(t, ctx, 2) + defer cleanup() + // Create <length> chunks of up to 4MB of random bytes. + data := make([][]byte, length) + for i := 0; i < length; i++ { + data[i] = testData(rg) + } + if err := binary.Create(ctx, int32(length), repository.MediaInfo{Type: "application/octet-stream"}); err != nil { + t.Fatalf("Create() failed: %v", err) + } + // Simulate a flaky upload client that keeps uploading parts until + // finished. + for { + parts, _, err := binary.Stat(ctx) + if err != nil { + t.Fatalf("Stat() failed: %v", err) + } + finished := true + for _, part := range parts { + finished = finished && (part != binarylib.MissingPart) + } + if finished { + break + } + for i := 0; i < length; i++ { + fail := rg.RandomIntn(2) + if parts[i] == binarylib.MissingPart && fail != 0 { + if streamErr, err := invokeUpload(t, ctx, binary, data[i], int32(i)); streamErr != nil || err != nil { + t.FailNow() + } + } + } + } + if err := binary.Delete(ctx); err != nil { + t.Fatalf("Delete() failed: %v", err) + } + } +} + +// TestErrors checks that the binary interface correctly reports errors. +func TestErrors(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + rg := testutil.NewRandGenerator(t.Logf) + + binary, endpoint, _, cleanup := startServer(t, ctx, 2) + defer cleanup() + const length = 2 + data := make([][]byte, length) + for i := 0; i < length; i++ { + data[i] = testData(rg) + for j := 0; j < len(data[i]); j++ { + data[i][j] = byte(rg.RandomInt()) + } + } + mediaInfo := repository.MediaInfo{Type: "application/octet-stream"} + if err := repository.BinaryClient(naming.JoinAddressName(endpoint, "")).Create(ctx, int32(length), mediaInfo); err == nil { + t.Fatalf("Create() did not fail on empty suffix when it should have") + } + if err := binary.Create(ctx, int32(length), mediaInfo); err != nil { + t.Fatalf("Create() failed: %v", err) + } + if err := binary.Create(ctx, int32(length), mediaInfo); err == nil { + t.Fatalf("Create() did not fail when it should have") + } else if want := verror.ErrExist.ID; verror.ErrorID(err) != want { + t.Fatalf("Unexpected error: %v, expected error id %v", err, want) + } + if streamErr, err := invokeUpload(t, ctx, binary, data[0], 0); streamErr != nil || err != nil { + t.Fatalf("Upload() failed: %v", err) + } + if _, err := invokeUpload(t, ctx, binary, data[0], 0); err == nil { + t.Fatalf("Upload() did not fail when it should have") + } else if want := verror.ErrExist.ID; verror.ErrorID(err) != want { + t.Fatalf("Unexpected error: %v, expected error id %v", err, want) + } + if _, _, err := invokeDownload(t, ctx, binary, 1); err == nil { + t.Fatalf("Download() did not fail when it should have") + } else if want := verror.ErrNoExist.ID; verror.ErrorID(err) != want { + t.Fatalf("Unexpected error: %v, expected error id %v", err, want) + } + if streamErr, err := invokeUpload(t, ctx, binary, data[1], 1); streamErr != nil || err != nil { + t.Fatalf("Upload() failed: %v", err) + } + if _, streamErr, err := invokeDownload(t, ctx, binary, 0); streamErr != nil || err != nil { + t.Fatalf("Download() failed: %v", err) + } + // Upload/Download on a part number that's outside the range set forth in + // Create should fail. + for _, part := range []int32{-1, length} { + if _, err := invokeUpload(t, ctx, binary, []byte("dummy"), part); err == nil { + t.Fatalf("Upload() did not fail when it should have") + } else if want := binarylib.ErrInvalidPart.ID; verror.ErrorID(err) != want { + t.Fatalf("Unexpected error: %v, expected error id %v", err, want) + } + if _, _, err := invokeDownload(t, ctx, binary, part); err == nil { + t.Fatalf("Download() did not fail when it should have") + } else if want := binarylib.ErrInvalidPart.ID; verror.ErrorID(err) != want { + t.Fatalf("Unexpected error: %v, expected error id %v", err, want) + } + } + if err := binary.Delete(ctx); err != nil { + t.Fatalf("Delete() failed: %v", err) + } + if err := binary.Delete(ctx); err == nil { + t.Fatalf("Delete() did not fail when it should have") + } else if want := verror.ErrNoExist.ID; verror.ErrorID(err) != want { + t.Fatalf("Unexpected error: %v, expected error id %v", err, want) + } +} + +func TestGlob(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + rg := testutil.NewRandGenerator(t.Logf) + + _, ep, _, cleanup := startServer(t, ctx, 2) + defer cleanup() + data := testData(rg) + + objects := []string{"foo", "bar", "hello world", "a/b/c"} + for _, obj := range objects { + name := naming.JoinAddressName(ep, obj) + binary := repository.BinaryClient(name) + + if err := binary.Create(ctx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil { + t.Fatalf("Create() failed: %v", err) + } + if streamErr, err := invokeUpload(t, ctx, binary, data, 0); streamErr != nil || err != nil { + t.FailNow() + } + } + results, _, err := testutil.GlobName(ctx, naming.JoinAddressName(ep, ""), "...") + if err != nil { + t.Fatalf("GlobName failed: %v", err) + } + expected := []string{"", "a", "a/b", "a/b/c", "bar", "foo", "hello world"} + if !reflect.DeepEqual(results, expected) { + t.Errorf("Unexpected results: expected %q, got %q", expected, results) + } +} diff --git a/x/ref/services/internal/binarylib/perms_test.go b/x/ref/services/internal/binarylib/perms_test.go new file mode 100644 index 000000000..687c6ed0c --- /dev/null +++ b/x/ref/services/internal/binarylib/perms_test.go @@ -0,0 +1,473 @@ +// Copyright 2015 The Vanadium 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 binarylib_test + +import ( + "fmt" + "reflect" + "testing" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/repository" + "v.io/v23/verror" + "v.io/x/lib/gosh" + "v.io/x/ref/lib/signals" + "v.io/x/ref/services/internal/binarylib" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" +) + +var binaryd = gosh.RegisterFunc("binaryd", func(publishName, storedir string) { + ctx, shutdown := test.V23Init() + defer shutdown() + + defer fmt.Printf("%v terminating\n", publishName) + defer ctx.VI(1).Infof("%v terminating", publishName) + + depth := 2 + state, err := binarylib.NewState(storedir, "", depth) + if err != nil { + ctx.Fatalf("NewState(%v, %v, %v) failed: %v", storedir, "", depth, err) + } + dispatcher, err := binarylib.NewDispatcher(ctx, state) + if err != nil { + ctx.Fatalf("Failed to create binaryd dispatcher: %v", err) + } + ctx, server, err := v23.WithNewDispatchingServer(ctx, publishName, dispatcher) + if err != nil { + ctx.Fatalf("NewDispatchingServer(%v) failed: %v", publishName, err) + } + ctx.VI(1).Infof("binaryd name: %v", server.Status().Endpoints[0].Name()) + + fmt.Println("READY") + <-signals.ShutdownOnSignals(ctx) +}) + +func b(name string) repository.BinaryClientStub { + return repository.BinaryClient(name) +} + +func ctxWithBlessedPrincipal(ctx *context.T, childExtension string) (*context.T, error) { + child := testutil.NewPrincipal() + if err := testutil.IDProviderFromPrincipal(v23.GetPrincipal(ctx)).Bless(child, childExtension); err != nil { + return nil, err + } + return v23.WithPrincipal(ctx, child) +} + +func TestBinaryCreateAccessList(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + rg := testutil.NewRandGenerator(t.Logf) + + selfCtx, err := v23.WithPrincipal(ctx, testutil.NewPrincipal("self")) + if err != nil { + t.Fatalf("WithPrincipal failed: %v", err) + } + childCtx, err := ctxWithBlessedPrincipal(selfCtx, "child") + if err != nil { + t.Fatalf("WithPrincipal failed: %v", err) + } + + sh, deferFn := servicetest.CreateShellAndMountTable(t, childCtx) + defer deferFn() + // make selfCtx and childCtx have the same Namespace Roots as set by + // CreateShellAndMountTable + v23.GetNamespace(selfCtx).SetRoots(v23.GetNamespace(childCtx).Roots()...) + + // setup mock up directory to put state in + storedir, cleanup := servicetest.SetupRootDir(t, "bindir") + defer cleanup() + prepDirectory(t, storedir) + + nmh := sh.FuncCmd(binaryd, "bini", storedir) + nmh.Start() + nmh.S.Expect("READY") + + ctx.VI(2).Infof("Self uploads a shared and private binary.") + if err := b("bini/private").Create(childCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil { + t.Fatalf("Create() failed %v", err) + } + fakeDataPrivate := testData(rg) + if streamErr, err := invokeUpload(t, childCtx, b("bini/private"), fakeDataPrivate, 0); streamErr != nil || err != nil { + t.Fatalf("invokeUpload() failed %v, %v", err, streamErr) + } + + ctx.VI(2).Infof("Validate that the AccessList also allows Self") + perms, _, err := b("bini/private").GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions failed: %v", err) + } + expectedInBps := []security.BlessingPattern{"self:$", "self:child"} + expected := access.Permissions{ + "Admin": access.AccessList{In: expectedInBps}, + "Read": access.AccessList{In: expectedInBps}, + "Write": access.AccessList{In: expectedInBps}, + "Debug": access.AccessList{In: expectedInBps}, + "Resolve": access.AccessList{In: expectedInBps}, + } + if got, want := perms.Normalize(), expected.Normalize(); !reflect.DeepEqual(got, want) { + t.Errorf("got %#v, expected %#v ", got, want) + } +} + +func TestBinaryRootAccessList(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + rg := testutil.NewRandGenerator(t.Logf) + + selfPrincipal := testutil.NewPrincipal("self") + selfBlessings, _ := selfPrincipal.BlessingStore().Default() + selfCtx, err := v23.WithPrincipal(ctx, selfPrincipal) + if err != nil { + t.Fatalf("WithPrincipal failed: %v", err) + } + sh, deferFn := servicetest.CreateShellAndMountTable(t, selfCtx) + defer deferFn() + + // setup mock up directory to put state in + storedir, cleanup := servicetest.SetupRootDir(t, "bindir") + defer cleanup() + prepDirectory(t, storedir) + + otherPrincipal := testutil.NewPrincipal("other") + if err := security.AddToRoots(otherPrincipal, selfBlessings); err != nil { + t.Fatalf("AddToRoots() failed: %v", err) + } + otherCtx, err := v23.WithPrincipal(selfCtx, otherPrincipal) + if err != nil { + t.Fatalf("WithPrincipal() failed: %v", err) + } + + nmh := sh.FuncCmd(binaryd, "bini", storedir) + nmh.Start() + nmh.S.Expect("READY") + + ctx.VI(2).Infof("Self uploads a shared and private binary.") + if err := b("bini/private").Create(selfCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil { + t.Fatalf("Create() failed %v", err) + } + fakeDataPrivate := testData(rg) + if streamErr, err := invokeUpload(t, selfCtx, b("bini/private"), fakeDataPrivate, 0); streamErr != nil || err != nil { + t.Fatalf("invokeUpload() failed %v, %v", err, streamErr) + } + + if err := b("bini/shared").Create(selfCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil { + t.Fatalf("Create() failed %v", err) + } + fakeDataShared := testData(rg) + if streamErr, err := invokeUpload(t, selfCtx, b("bini/shared"), fakeDataShared, 0); streamErr != nil || err != nil { + t.Fatalf("invokeUpload() failed %v, %v", err, streamErr) + } + + ctx.VI(2).Infof("Verify that in the beginning other can't access bini/private or bini/shared") + if _, _, err := b("bini/private").Stat(otherCtx); verror.ErrorID(err) != verror.ErrNoAccess.ID { + t.Fatalf("Stat() should have failed but didn't: %v", err) + } + if _, _, err := b("bini/shared").Stat(otherCtx); verror.ErrorID(err) != verror.ErrNoAccess.ID { + t.Fatalf("Stat() should have failed but didn't: %v", err) + } + + ctx.VI(2).Infof("Validate the AccessList file on bini/private.") + perms, _, err := b("bini/private").GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions failed: %v", err) + } + expectedInBps := []security.BlessingPattern{"self"} + expected := access.Permissions{ + "Admin": access.AccessList{In: expectedInBps}, + "Read": access.AccessList{In: expectedInBps}, + "Write": access.AccessList{In: expectedInBps}, + "Debug": access.AccessList{In: expectedInBps}, + "Resolve": access.AccessList{In: expectedInBps}, + } + if got, want := perms.Normalize(), expected.Normalize(); !reflect.DeepEqual(got, want) { + t.Errorf("got %#v, expected %#v ", got, want) + } + + ctx.VI(2).Infof("Validate the AccessList file on bini/private.") + perms, version, err := b("bini/private").GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions failed: %v", err) + } + if got, want := perms.Normalize(), expected.Normalize(); !reflect.DeepEqual(got, want) { + t.Errorf("got %#v, expected %#v ", got, want) + } + + ctx.VI(2).Infof("self blesses other as self/other and locks the bini/private binary to itself.") + selfBlessing, _ := selfPrincipal.BlessingStore().Default() + otherBlessing, err := selfPrincipal.Bless(otherPrincipal.PublicKey(), selfBlessing, "other", security.UnconstrainedUse()) + if err != nil { + t.Fatalf("selfPrincipal.Bless() failed: %v", err) + } + if _, err := otherPrincipal.BlessingStore().Set(otherBlessing, security.AllPrincipals); err != nil { + t.Fatalf("otherPrincipal.BlessingStore() failed: %v", err) + } + + ctx.VI(2).Infof("Self modifies the AccessList file on bini/private.") + for _, tag := range access.AllTypicalTags() { + perms.Clear("self", string(tag)) + perms.Add("self:$", string(tag)) + } + if err := b("bini/private").SetPermissions(selfCtx, perms, version); err != nil { + t.Fatalf("SetPermissions failed: %v", err) + } + + ctx.VI(2).Infof(" Verify that bini/private's perms are updated.") + updatedInBps := []security.BlessingPattern{"self:$"} + updated := access.Permissions{ + "Admin": access.AccessList{In: updatedInBps}, + "Read": access.AccessList{In: updatedInBps}, + "Write": access.AccessList{In: updatedInBps}, + "Debug": access.AccessList{In: updatedInBps}, + "Resolve": access.AccessList{In: updatedInBps}, + } + perms, _, err = b("bini/private").GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions failed: %v", err) + } + if got, want := perms.Normalize(), updated.Normalize(); !reflect.DeepEqual(got, want) { + t.Errorf("got %#v, expected %#v ", got, want) + } + + // Other still can't access bini/shared because there's no AccessList file at the + // root level. Self has to set one explicitly to enable sharing. This way, self + // can't accidentally expose the server without setting a root AccessList. + ctx.VI(2).Infof(" Verify that other still can't access bini/shared.") + if _, _, err := b("bini/shared").Stat(otherCtx); verror.ErrorID(err) != verror.ErrNoAccess.ID { + t.Fatalf("Stat() should have failed but didn't: %v", err) + } + + ctx.VI(2).Infof("Self sets a root AccessList.") + newRootAccessList := make(access.Permissions) + for _, tag := range access.AllTypicalTags() { + newRootAccessList.Add("self:$", string(tag)) + } + if err := b("bini").SetPermissions(selfCtx, newRootAccessList, ""); err != nil { + t.Fatalf("SetPermissions failed: %v", err) + } + + ctx.VI(2).Infof("Verify that other can access bini/shared now but not access bini/private.") + if _, _, err := b("bini/shared").Stat(otherCtx); err != nil { + t.Fatalf("Stat() shouldn't have failed: %v", err) + } + if _, _, err := b("bini/private").Stat(otherCtx); verror.ErrorID(err) != verror.ErrNoAccess.ID { + t.Fatalf("Stat() should have failed but didn't: %v", err) + } + + ctx.VI(2).Infof("Other still can't create so Self gives Other right to Create.") + perms, tag, err := b("bini").GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions() failed: %v", err) + } + + // More than one AccessList change will result in the same functional result in + // this test: that self:other acquires the right to invoke Create at the + // root. In particular: + // + // a. perms.Add("self", "Write ") + // b. perms.Add("self:other", "Write") + // c. perms.Add("self:other:$", "Write") + // + // will all give self:other the right to invoke Create but in the case of + // (a) it will also extend this right to self's delegates (because of the + // absence of the $) including other and in (b) will also extend the + // Create right to all of other's delegates. Since (c) is the minimum + // case, use that. + perms.Add("self:other:$", string("Write")) + err = b("bini").SetPermissions(selfCtx, perms, tag) + if err != nil { + t.Fatalf("SetPermissions() failed: %v", err) + } + + ctx.VI(2).Infof("Other creates bini/otherbinary") + if err := b("bini/otherbinary").Create(otherCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); err != nil { + t.Fatalf("Create() failed %v", err) + } + fakeDataOther := testData(rg) + if streamErr, err := invokeUpload(t, otherCtx, b("bini/otherbinary"), fakeDataOther, 0); streamErr != nil || err != nil { + t.FailNow() + } + + ctx.VI(2).Infof("Other can read perms for bini/otherbinary.") + updatedInBps = []security.BlessingPattern{"self:$", "self:other"} + updated = access.Permissions{ + "Admin": access.AccessList{In: updatedInBps}, + "Read": access.AccessList{In: updatedInBps}, + "Write": access.AccessList{In: updatedInBps}, + "Debug": access.AccessList{In: updatedInBps}, + "Resolve": access.AccessList{In: updatedInBps}, + } + perms, _, err = b("bini/otherbinary").GetPermissions(otherCtx) + if err != nil { + t.Fatalf("GetPermissions failed: %v", err) + } + if got, want := perms.Normalize(), updated.Normalize(); !reflect.DeepEqual(want, got) { + t.Errorf("got %#v, expected %#v ", got, want) + } + + ctx.VI(2).Infof("Other tries to exclude self by removing self from the AccessList set") + perms, tag, err = b("bini/otherbinary").GetPermissions(otherCtx) + if err != nil { + t.Fatalf("GetPermissions() failed: %v", err) + } + perms.Clear("self:$") + err = b("bini/otherbinary").SetPermissions(otherCtx, perms, tag) + if err != nil { + t.Fatalf("SetPermissions() failed: %v", err) + } + + ctx.VI(2).Infof("Verify that other can make this change.") + updatedInBps = []security.BlessingPattern{"self:other"} + updated = access.Permissions{ + "Admin": access.AccessList{In: updatedInBps}, + "Read": access.AccessList{In: updatedInBps}, + "Write": access.AccessList{In: updatedInBps}, + "Debug": access.AccessList{In: updatedInBps}, + "Resolve": access.AccessList{In: updatedInBps}, + } + perms, _, err = b("bini/otherbinary").GetPermissions(otherCtx) + if err != nil { + t.Fatalf("GetPermissions failed: %v", err) + } + if got, want := perms.Normalize(), updated.Normalize(); !reflect.DeepEqual(want, got) { + t.Errorf("got %#v, expected %#v ", got, want) + } + + ctx.VI(2).Infof("But self's rights are inherited from root so self can still access despite this.") + if _, _, err := b("bini/otherbinary").Stat(selfCtx); err != nil { + t.Fatalf("Stat() shouldn't have failed: %v", err) + } + + ctx.VI(2).Infof("Self petulantly blacklists other back.") + perms, tag, err = b("bini").GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions() failed: %v", err) + } + for _, tag := range access.AllTypicalTags() { + perms.Blacklist("self:other", string(tag)) + } + err = b("bini").SetPermissions(selfCtx, perms, tag) + if err != nil { + t.Fatalf("SetPermissions() failed: %v", err) + } + + ctx.VI(2).Infof("And now other can do nothing at affecting the root. Other should be penitent.") + if err := b("bini/nototherbinary").Create(otherCtx, 1, repository.MediaInfo{Type: "application/octet-stream"}); verror.ErrorID(err) != verror.ErrNoAccess.ID { + t.Fatalf("Create() should have failed %v", err) + } + + ctx.VI(2).Infof("But other can still access shared.") + if _, _, err := b("bini/shared").Stat(otherCtx); err != nil { + t.Fatalf("Stat() should not have failed but did: %v", err) + } + + ctx.VI(2).Infof("Self petulantly blacklists other's binary too.") + perms, tag, err = b("bini/shared").GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions() failed: %v", err) + } + for _, tag := range access.AllTypicalTags() { + perms.Blacklist("self:other", string(tag)) + } + err = b("bini/shared").SetPermissions(selfCtx, perms, tag) + if err != nil { + t.Fatalf("SetPermissions() failed: %v", err) + } + ctx.VI(2).Infof("And now other can't access shared either.") + if _, _, err := b("bini/shared").Stat(otherCtx); verror.ErrorID(err) != verror.ErrNoAccess.ID { + t.Fatalf("Stat() should have failed but didn't: %v", err) + } + // TODO(rjkroege): Extend the test with a third principal and verify that + // new principals can be given Admin perimission at the root. + + ctx.VI(2).Infof("Self feels guilty for petulance and disempowers itself") + // TODO(rjkroege,caprita): This is a one-way transition for self. Perhaps it + // should not be. Consider adding a factory-reset facility. + perms, tag, err = b("bini").GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions() failed: %v", err) + } + perms.Clear("self:$", "Admin") + err = b("bini").SetPermissions(selfCtx, perms, tag) + if err != nil { + t.Fatalf("SetPermissions() failed: %v", err) + } + + ctx.VI(2).Info("Self can't access other's binary now") + if _, _, err := b("bini/otherbinary").Stat(selfCtx); err == nil { + t.Fatalf("Stat() should have failed but didn't") + } +} + +func TestBinaryRationalStartingValueForGetPermissions(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + selfPrincipal := testutil.NewPrincipal("self") + selfBlessings, _ := selfPrincipal.BlessingStore().Default() + selfCtx, err := v23.WithPrincipal(ctx, selfPrincipal) + if err != nil { + t.Fatalf("WithPrincipal failed: %v", err) + } + sh, deferFn := servicetest.CreateShellAndMountTable(t, selfCtx) + defer deferFn() + + // setup mock up directory to put state in + storedir, cleanup := servicetest.SetupRootDir(t, "bindir") + defer cleanup() + prepDirectory(t, storedir) + + otherPrincipal := testutil.NewPrincipal("other") + if err := security.AddToRoots(otherPrincipal, selfBlessings); err != nil { + t.Fatalf("AddToRoots() failed: %v", err) + } + + nmh := sh.FuncCmd(binaryd, "bini", storedir) + nmh.Start() + nmh.S.Expect("READY") + + perms, tag, err := b("bini").GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions failed: %#v", err) + } + expectedInBps := []security.BlessingPattern{"self:$", "self:child"} + expected := access.Permissions{ + "Admin": access.AccessList{In: expectedInBps, NotIn: []string{}}, + "Read": access.AccessList{In: expectedInBps, NotIn: []string{}}, + "Write": access.AccessList{In: expectedInBps, NotIn: []string{}}, + "Debug": access.AccessList{In: expectedInBps, NotIn: []string{}}, + "Resolve": access.AccessList{In: expectedInBps, NotIn: []string{}}, + } + if got, want := perms.Normalize(), expected.Normalize(); !reflect.DeepEqual(got, want) { + t.Errorf("got %#v, expected %#v ", got, want) + } + + perms.Blacklist("self", string("Read")) + err = b("bini").SetPermissions(selfCtx, perms, tag) + if err != nil { + t.Fatalf("SetPermissions() failed: %v", err) + } + + perms, tag, err = b("bini").GetPermissions(selfCtx) + if err != nil { + t.Fatalf("GetPermissions failed: %#v", err) + } + expectedInBps = []security.BlessingPattern{"self:$", "self:child"} + expected = access.Permissions{ + "Admin": access.AccessList{In: expectedInBps, NotIn: []string{}}, + "Read": access.AccessList{In: expectedInBps, NotIn: []string{"self"}}, + "Write": access.AccessList{In: expectedInBps, NotIn: []string{}}, + "Debug": access.AccessList{In: expectedInBps, NotIn: []string{}}, + "Resolve": access.AccessList{In: expectedInBps, NotIn: []string{}}, + } + if got, want := perms.Normalize(), expected.Normalize(); !reflect.DeepEqual(got, want) { + t.Errorf("got %#v, expected %#v ", got, want) + } +} diff --git a/x/ref/services/internal/binarylib/service.go b/x/ref/services/internal/binarylib/service.go new file mode 100644 index 000000000..e7d88cebf --- /dev/null +++ b/x/ref/services/internal/binarylib/service.go @@ -0,0 +1,437 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The implementation of the binary repository interface stores objects +// identified by object name suffixes using the local file system. Given an +// object name suffix, the implementation computes an MD5 hash of the suffix and +// generates the following path in the local filesystem: +// /<root-dir>/<dir_1>/.../<dir_n>/<hash>. The root directory and the directory +// depth are parameters of the implementation. <root-dir> also contains +// __acls/data and __acls/sig files storing the Permissions for the root level. +// The contents of the directory include the checksum and data for each of the +// individual parts of the binary, the name of the object and a directory +// containing the perms for this particular object: +// +// name +// acls/data +// acls/sig +// mediainfo +// name +// <part_1>/checksum +// <part_1>/data +// ... +// <part_n>/checksum +// <part_n>/data +// +// TODO(jsimsa): Add an "fsck" method that cleans up existing on-disk +// repository and provide a command-line flag that identifies whether +// fsck should run when new repository server process starts up. +package binarylib + +import ( + "crypto/md5" + "encoding/hex" + "encoding/json" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "syscall" + + "v.io/v23/context" + "v.io/v23/glob" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/binary" + "v.io/v23/services/repository" + "v.io/v23/verror" + "v.io/x/ref/services/internal/pathperms" +) + +// binaryService implements the Binary server interface. +type binaryService struct { + // path is the local filesystem path to the object identified by the + // object name suffix. + path string + // state holds the state shared across different binary repository + // invocations. + state *state + // suffix is the name of the binary object. + suffix string + permsStore *pathperms.PathStore +} + +const pkgPath = "v.io/x/ref/services/internal/binarylib" + +var ( + ErrInProgress = verror.Register(pkgPath+".errInProgress", verror.NoRetry, "{1:}{2:} identical upload already in progress{:_}") + ErrInvalidParts = verror.Register(pkgPath+".errInvalidParts", verror.NoRetry, "{1:}{2:} invalid number of binary parts{:_}") + ErrInvalidPart = verror.Register(pkgPath+".errInvalidPart", verror.NoRetry, "{1:}{2:} invalid binary part number{:_}") + ErrOperationFailed = verror.Register(pkgPath+".errOperationFailed", verror.NoRetry, "{1:}{2:} operation failed{:_}") + ErrNotAuthorized = verror.Register(pkgPath+".errNotAuthorized", verror.NoRetry, "{1:}{2:} none of the client's blessings are valid {:_}") + ErrInvalidSuffix = verror.Register(pkgPath+".errInvalidSuffix", verror.NoRetry, "{1:}{2:} invalid suffix{:_}") +) + +// TODO(jsimsa): When VDL supports composite literal constants, remove +// this definition. +var MissingPart = binary.PartInfo{ + Checksum: binary.MissingChecksum, + Size: binary.MissingSize, +} + +// newBinaryService returns a new Binary service implementation. +func newBinaryService(state *state, suffix string, permsStore *pathperms.PathStore) *binaryService { + return &binaryService{ + path: state.dir(suffix), + state: state, + suffix: suffix, + permsStore: permsStore, + } +} + +const BufferLength = 4096 + +func (i *binaryService) createFileTree(ctx *context.T, nparts int32, mediaInfo repository.MediaInfo) (string, error) { + parent, dirPerm := filepath.Dir(i.path), os.FileMode(0700) + if err := os.MkdirAll(parent, dirPerm); err != nil { + ctx.Errorf("MkdirAll(%v, %v) failed: %v", parent, dirPerm, err) + return "", verror.New(ErrOperationFailed, ctx) + } + prefix := "creating-" + tmpDir, err := ioutil.TempDir(parent, prefix) + if err != nil { + ctx.Errorf("TempDir(%v, %v) failed: %v", parent, prefix, err) + return "", verror.New(ErrOperationFailed, ctx) + } + nameFile, filePerm := filepath.Join(tmpDir, nameFileName), os.FileMode(0600) + if err := ioutil.WriteFile(nameFile, []byte(i.suffix), filePerm); err != nil { + ctx.Errorf("WriteFile(%q) failed: %v", nameFile, err) + return "", verror.New(ErrOperationFailed, ctx) + } + infoFile := filepath.Join(tmpDir, mediaInfoFileName) + jInfo, err := json.Marshal(mediaInfo) + if err != nil { + ctx.Errorf("json.Marshal(%v) failed: %v", mediaInfo, err) + return "", verror.New(ErrOperationFailed, ctx) + } + if err := ioutil.WriteFile(infoFile, jInfo, filePerm); err != nil { + ctx.Errorf("WriteFile(%q) failed: %v", infoFile, err) + return "", verror.New(ErrOperationFailed, ctx) + } + for j := 0; j < int(nparts); j++ { + partPath := generatePartPath(tmpDir, j) + if err := os.MkdirAll(partPath, dirPerm); err != nil { + ctx.Errorf("MkdirAll(%v, %v) failed: %v", partPath, dirPerm, err) + if err := os.RemoveAll(tmpDir); err != nil { + ctx.Errorf("RemoveAll(%v) failed: %v", tmpDir, err) + } + return "", verror.New(ErrOperationFailed, ctx) + } + } + return tmpDir, nil +} + +func (i *binaryService) deleteACLs(ctx *context.T) error { + permsDir := permsPath(i.state.rootDir, i.suffix) + if err := i.permsStore.Delete(permsDir); err != nil { + return err + } + // HACK: we need to also clean up the parent directory (corresponding to + // the suffix) that holds the "acls" directory for regular binary + // objects. See the implementation of permsPath. + if base := filepath.Base(permsDir); base == "acls" { + if err := os.Remove(filepath.Dir(permsDir)); err != nil { + return err + } + } + return nil +} + +// setInitialPermissions sets the acls for the binary if they don't exist (if +// they do, it's a sign that the binary already exists, and then an error is +// returned). Upon success, it returns a function that removes the permissions +// just set here (to be called if something fails downstream and we need to undo +// setting the initial permissions). +func (i *binaryService) setInitialPermissions(ctx *context.T, call rpc.ServerCall) (func(), error) { + rb, _ := security.RemoteBlessingNames(ctx, call.Security()) + if len(rb) == 0 { + // None of the client's blessings are valid. + return nil, verror.New(ErrNotAuthorized, ctx) + } + permsDir := permsPath(i.state.rootDir, i.suffix) + created, err := i.permsStore.SetIfAbsent(permsDir, pathperms.PermissionsForBlessings(rb)) + if err != nil { + ctx.Errorf("permsStore.SetIfAbsent(%v, %v) failed: %v", permsDir, rb, err) + return nil, verror.New(ErrOperationFailed, ctx) + } + if !created { + return nil, verror.New(verror.ErrExist, ctx, i.suffix) + } + return func() { + if err := i.deleteACLs(ctx); err != nil { + ctx.Errorf("deleteACLs() failed: %v", err) + } + }, nil +} + +func (i *binaryService) Create(ctx *context.T, call rpc.ServerCall, nparts int32, mediaInfo repository.MediaInfo) error { + ctx.Infof("%v.Create(%v, %v)", i.suffix, nparts, mediaInfo) + // Disallow creating binaries on the root of the server. The + // permissions on the root have special meaning (see + // hierarchical_authorizer). + if i.suffix == "" { + return verror.New(ErrInvalidSuffix, ctx, "") + } + if nparts < 1 { + return verror.New(ErrInvalidParts, ctx) + } + removePerms, err := i.setInitialPermissions(ctx, call) + if err != nil { + return err + } + tmpDir, err := i.createFileTree(ctx, nparts, mediaInfo) + if err != nil { + removePerms() + return err + } + // Use os.Rename() to atomically create the binary directory + // structure. + if err := os.Rename(tmpDir, i.path); err != nil { + ctx.Errorf("Rename(%v, %v) failed: %v", tmpDir, i.path, err) + if err := os.RemoveAll(tmpDir); err != nil { + ctx.Errorf("RemoveAll(%v) failed: %v", tmpDir, err) + } + removePerms() + return verror.New(ErrOperationFailed, ctx, i.path) + } + return nil +} + +func (i *binaryService) Delete(ctx *context.T, _ rpc.ServerCall) error { + ctx.Infof("%v.Delete()", i.suffix) + if _, err := os.Stat(i.path); err != nil { + if os.IsNotExist(err) { + return verror.New(verror.ErrNoExist, ctx, i.path) + } + ctx.Errorf("Stat(%v) failed: %v", i.path, err) + return verror.New(ErrOperationFailed, ctx) + } + // Use os.Rename() to atomically remove the binary directory + // structure. + path := filepath.Join(filepath.Dir(i.path), "removing-"+filepath.Base(i.path)) + if err := os.Rename(i.path, path); err != nil { + ctx.Errorf("Rename(%v, %v) failed: %v", i.path, path, err) + return verror.New(ErrOperationFailed, ctx, i.path) + } + if err := os.RemoveAll(path); err != nil { + ctx.Errorf("Remove(%v) failed: %v", path, err) + return verror.New(ErrOperationFailed, ctx) + } + for { + // Remove the binary and all directories on the path back to the + // root directory that are left empty after the binary is removed. + path = filepath.Dir(path) + if i.state.rootDir == path { + break + } + if err := os.Remove(path); err != nil { + if err.(*os.PathError).Err.Error() == syscall.ENOTEMPTY.Error() { + break + } + ctx.Errorf("Remove(%v) failed: %v", path, err) + return verror.New(ErrOperationFailed, ctx) + } + } + if err := i.deleteACLs(ctx); err != nil { + ctx.Errorf("deleteACLs() failed: %v", err) + return verror.New(ErrOperationFailed, ctx) + } + return nil +} + +func (i *binaryService) Download(ctx *context.T, call repository.BinaryDownloadServerCall, part int32) error { + ctx.Infof("%v.Download(%v)", i.suffix, part) + path := i.generatePartPath(int(part)) + if err := checksumExists(ctx, path); err != nil { + return err + } + dataPath := filepath.Join(path, dataFileName) + file, err := os.Open(dataPath) + if err != nil { + ctx.Errorf("Open(%v) failed: %v", dataPath, err) + return verror.New(ErrOperationFailed, ctx) + } + defer file.Close() + buffer := make([]byte, BufferLength) + sender := call.SendStream() + for { + n, err := file.Read(buffer) + if err != nil && err != io.EOF { + ctx.Errorf("Read() failed: %v", err) + return verror.New(ErrOperationFailed, ctx) + } + if n == 0 { + break + } + if err := sender.Send(buffer[:n]); err != nil { + ctx.Errorf("Send() failed: %v", err) + return verror.New(ErrOperationFailed, ctx) + } + } + return nil +} + +// TODO(jsimsa): Design and implement an access control mechanism for +// the URL-based downloads. +func (i *binaryService) DownloadUrl(ctx *context.T, _ rpc.ServerCall) (string, int64, error) { + ctx.Infof("%v.DownloadUrl()", i.suffix) + return i.state.rootURL + "/" + i.suffix, 0, nil +} + +func (i *binaryService) Stat(ctx *context.T, _ rpc.ServerCall) ([]binary.PartInfo, repository.MediaInfo, error) { + ctx.Infof("%v.Stat()", i.suffix) + result := make([]binary.PartInfo, 0) + parts, err := getParts(ctx, i.path) + if err != nil { + return []binary.PartInfo{}, repository.MediaInfo{}, err + } + for _, part := range parts { + checksumFile := filepath.Join(part, checksumFileName) + bytes, err := ioutil.ReadFile(checksumFile) + if err != nil { + if os.IsNotExist(err) { + result = append(result, MissingPart) + continue + } + ctx.Errorf("ReadFile(%v) failed: %v", checksumFile, err) + return []binary.PartInfo{}, repository.MediaInfo{}, verror.New(ErrOperationFailed, ctx) + } + dataFile := filepath.Join(part, dataFileName) + fi, err := os.Stat(dataFile) + if err != nil { + if os.IsNotExist(err) { + result = append(result, MissingPart) + continue + } + ctx.Errorf("Stat(%v) failed: %v", dataFile, err) + return []binary.PartInfo{}, repository.MediaInfo{}, verror.New(ErrOperationFailed, ctx) + } + result = append(result, binary.PartInfo{Checksum: string(bytes), Size: fi.Size()}) + } + infoFile := filepath.Join(i.path, mediaInfoFileName) + jInfo, err := ioutil.ReadFile(infoFile) + if err != nil { + ctx.Errorf("ReadFile(%q) failed: %v", infoFile, err) + return []binary.PartInfo{}, repository.MediaInfo{}, verror.New(ErrOperationFailed, ctx) + } + var mediaInfo repository.MediaInfo + if err := json.Unmarshal(jInfo, &mediaInfo); err != nil { + ctx.Errorf("json.Unmarshal(%v) failed: %v", jInfo, err) + return []binary.PartInfo{}, repository.MediaInfo{}, verror.New(ErrOperationFailed, ctx) + } + return result, mediaInfo, nil +} + +func (i *binaryService) Upload(ctx *context.T, call repository.BinaryUploadServerCall, part int32) error { + ctx.Infof("%v.Upload(%v)", i.suffix, part) + path, suffix := i.generatePartPath(int(part)), "" + err := checksumExists(ctx, path) + if err == nil { + return verror.New(verror.ErrExist, ctx, path) + } else if verror.ErrorID(err) != verror.ErrNoExist.ID { + return err + } + // Use os.OpenFile() to resolve races. + lockPath, flags, perm := filepath.Join(path, lockFileName), os.O_CREATE|os.O_WRONLY|os.O_EXCL, os.FileMode(0600) + lockFile, err := os.OpenFile(lockPath, flags, perm) + if err != nil { + if os.IsExist(err) { + return verror.New(ErrInProgress, ctx, path) + } + ctx.Errorf("OpenFile(%v, %v, %v) failed: %v", lockPath, flags, suffix, err) + return verror.New(ErrOperationFailed, ctx) + } + defer os.Remove(lockFile.Name()) + defer lockFile.Close() + file, err := ioutil.TempFile(path, suffix) + if err != nil { + ctx.Errorf("TempFile(%v, %v) failed: %v", path, suffix, err) + return verror.New(ErrOperationFailed, ctx) + } + defer file.Close() + h := md5.New() + rStream := call.RecvStream() + for rStream.Advance() { + bytes := rStream.Value() + if _, err := file.Write(bytes); err != nil { + ctx.Errorf("Write() failed: %v", err) + if err := os.Remove(file.Name()); err != nil { + ctx.Errorf("Remove(%v) failed: %v", file.Name(), err) + } + return verror.New(ErrOperationFailed, ctx) + } + h.Write(bytes) + } + + if err := rStream.Err(); err != nil { + ctx.Errorf("Advance() failed: %v", err) + if err := os.Remove(file.Name()); err != nil { + ctx.Errorf("Remove(%v) failed: %v", file.Name(), err) + } + return verror.New(ErrOperationFailed, ctx) + } + + hash := hex.EncodeToString(h.Sum(nil)) + checksumFile, perm := filepath.Join(path, checksumFileName), os.FileMode(0600) + if err := ioutil.WriteFile(checksumFile, []byte(hash), perm); err != nil { + ctx.Errorf("WriteFile(%v, %v, %v) failed: %v", checksumFile, hash, perm, err) + if err := os.Remove(file.Name()); err != nil { + ctx.Errorf("Remove(%v) failed: %v", file.Name(), err) + } + return verror.New(ErrOperationFailed, ctx) + } + dataFile := filepath.Join(path, dataFileName) + if err := os.Rename(file.Name(), dataFile); err != nil { + ctx.Errorf("Rename(%v, %v) failed: %v", file.Name(), dataFile, err) + if err := os.Remove(file.Name()); err != nil { + ctx.Errorf("Remove(%v) failed: %v", file.Name(), err) + } + return verror.New(ErrOperationFailed, ctx) + } + return nil +} + +func (i *binaryService) GlobChildren__(ctx *context.T, call rpc.GlobChildrenServerCall, m *glob.Element) error { + elems := strings.Split(i.suffix, "/") + if len(elems) == 1 && elems[0] == "" { + elems = nil + } + n := i.createObjectNameTree().find(elems, false) + if n == nil { + return verror.New(ErrOperationFailed, ctx) + } + for k, _ := range n.children { + if m.Match(k) { + call.SendStream().Send(naming.GlobChildrenReplyName{Value: k}) + } + } + return nil +} + +func (i *binaryService) GetPermissions(ctx *context.T, call rpc.ServerCall) (perms access.Permissions, version string, err error) { + perms, version, err = i.permsStore.Get(permsPath(i.state.rootDir, i.suffix)) + if os.IsNotExist(err) { + // No Permissions file found which implies a nil authorizer. This results in + // default authorization. + return pathperms.NilAuthPermissions(ctx, call.Security()), "", nil + } + return perms, version, err +} + +func (i *binaryService) SetPermissions(_ *context.T, _ rpc.ServerCall, perms access.Permissions, version string) error { + return i.permsStore.Set(permsPath(i.state.rootDir, i.suffix), perms, version) +} diff --git a/x/ref/services/internal/binarylib/setup.go b/x/ref/services/internal/binarylib/setup.go new file mode 100644 index 000000000..e01ab2b79 --- /dev/null +++ b/x/ref/services/internal/binarylib/setup.go @@ -0,0 +1,53 @@ +// Copyright 2015 The Vanadium 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 binarylib + +import ( + "io/ioutil" + "os" + "path/filepath" + + "v.io/x/ref/internal/logger" +) + +const defaultRootPrefix = "veyron_binary_repository" + +// SetupRootDir sets up the root directory if it doesn't already exist. If an +// empty string is used as root, create a new temporary directory. +func SetupRootDir(root string) (string, error) { + if root == "" { + var err error + if root, err = ioutil.TempDir("", defaultRootPrefix); err != nil { + logger.Global().Errorf("TempDir() failed: %v\n", err) + return "", err + } + path, perm := filepath.Join(root, VersionFile), os.FileMode(0600) + if err := ioutil.WriteFile(path, []byte(Version), perm); err != nil { + logger.Global().Errorf("WriteFile(%v, %v, %v) failed: %v", path, Version, perm, err) + return "", err + } + return root, nil + } + + _, err := os.Stat(root) + switch { + case err == nil: + case os.IsNotExist(err): + perm := os.FileMode(0700) + if err := os.MkdirAll(root, perm); err != nil { + logger.Global().Errorf("MkdirAll(%v, %v) failed: %v", root, perm, err) + return "", err + } + path, perm := filepath.Join(root, VersionFile), os.FileMode(0600) + if err := ioutil.WriteFile(path, []byte(Version), perm); err != nil { + logger.Global().Errorf("WriteFile(%v, %v, %v) failed: %v", path, Version, perm, err) + return "", err + } + default: + logger.Global().Errorf("Stat(%v) failed: %v", root, err) + return "", err + } + return root, nil +} diff --git a/x/ref/services/internal/binarylib/state.go b/x/ref/services/internal/binarylib/state.go new file mode 100644 index 000000000..391bda200 --- /dev/null +++ b/x/ref/services/internal/binarylib/state.go @@ -0,0 +1,86 @@ +// Copyright 2015 The Vanadium 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 binarylib + +import ( + "crypto/md5" + "encoding/hex" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "v.io/v23/verror" +) + +var ( + errUnexpectedDepth = verror.Register(pkgPath+".errUnexpectedDepth", verror.NoRetry, "{1:}{2:} Unexpected depth, expected a value between {3} and {4}, got {5}{:_}") + errStatFailed = verror.Register(pkgPath+".errStatFailed", verror.NoRetry, "{1:}{2:} Stat({3}) failed{:_}") + errReadFileFailed = verror.Register(pkgPath+".errReadFileFailed", verror.NoRetry, "{1:}{2:} ReadFile({3}) failed{:_}") + errUnexpectedVersion = verror.Register(pkgPath+".errUnexpectedVersion", verror.NoRetry, "{1:}{2:} Unexpected version: expected {3}, got {4}{:_}") +) + +// state holds the state shared across different binary repository +// invocations. +type state struct { + // depth determines the depth of the directory hierarchy that the + // binary repository uses to organize binaries in the local file + // system. There is a trade-off here: smaller values lead to faster + // access, while higher values allow the performance to scale to + // larger collections of binaries. The number should be a value + // between 0 and (md5.Size - 1). + // + // Note that the cardinality of each level (except the leaf level) + // is at most 256. If you expect to have X total binary items, and + // you want to limit directories to at most Y entries (because of + // filesystem limitations), then you should set depth to at least + // log_256(X/Y). For example, using hierarchyDepth = 3 with a local + // filesystem that can handle up to 1,000 entries per directory + // before its performance degrades allows the binary repository to + // store 16B objects. + depth int + // rootDir identifies the local filesystem directory in which the + // binary repository stores its objects. + rootDir string + // rootURL identifies the root URL of the HTTP server serving + // the download URLs. + rootURL string +} + +// NewState creates a new state object for the binary service. This +// should be passed into both NewDispatcher and NewHTTPRoot. +func NewState(rootDir, rootURL string, depth int) (*state, error) { + if min, max := 0, md5.Size-1; min > depth || depth > max { + return nil, verror.New(errUnexpectedDepth, nil, min, max, depth) + } + if _, err := os.Stat(rootDir); err != nil { + return nil, verror.New(errStatFailed, nil, rootDir, err) + } + path := filepath.Join(rootDir, VersionFile) + output, err := ioutil.ReadFile(path) + if err != nil { + return nil, verror.New(errReadFileFailed, nil, path, err) + } + if expected, got := Version, strings.TrimSpace(string(output)); expected != got { + return nil, verror.New(errUnexpectedVersion, nil, expected, got) + } + return &state{ + depth: depth, + rootDir: rootDir, + rootURL: rootURL, + }, nil +} + +// dir generates the local filesystem path for the binary identified by suffix. +func (s *state) dir(suffix string) string { + h := md5.New() + h.Write([]byte(suffix)) + hash := hex.EncodeToString(h.Sum(nil)) + dir := "" + for j := 0; j < s.depth; j++ { + dir = filepath.Join(dir, hash[j*2:(j+1)*2]) + } + return filepath.Join(s.rootDir, dir, hash) +} diff --git a/x/ref/services/internal/binarylib/util_test.go b/x/ref/services/internal/binarylib/util_test.go new file mode 100644 index 000000000..6e7fc6a9f --- /dev/null +++ b/x/ref/services/internal/binarylib/util_test.go @@ -0,0 +1,95 @@ +// Copyright 2015 The Vanadium 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 binarylib_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "v.io/v23/context" + "v.io/v23/services/repository" + + "v.io/x/ref/services/internal/binarylib" + "v.io/x/ref/test/testutil" +) + +// invokeUpload invokes the Upload RPC using the given client binary +// <binary> and streams the given binary <binary> to it. +func invokeUpload(t *testing.T, ctx *context.T, binary repository.BinaryClientMethods, data []byte, part int32) (error, error) { + stream, err := binary.Upload(ctx, part) + if err != nil { + t.Errorf("Upload() failed: %v", err) + return nil, err + } + sender := stream.SendStream() + if streamErr := sender.Send(data); streamErr != nil { + err := stream.Finish() + if err != nil { + t.Logf("Finish() failed: %v", err) + } + t.Logf("Send() failed: %v", streamErr) + return streamErr, err + } + if streamErr := sender.Close(); streamErr != nil { + err := stream.Finish() + if err != nil { + t.Logf("Finish() failed: %v", err) + } + t.Logf("Close() failed: %v", streamErr) + return streamErr, err + } + if err := stream.Finish(); err != nil { + t.Logf("Finish() failed: %v", err) + return nil, err + } + return nil, nil +} + +// invokeDownload invokes the Download RPC using the given client binary +// <binary> and streams binary from to it. +func invokeDownload(t *testing.T, ctx *context.T, binary repository.BinaryClientMethods, part int32) ([]byte, error, error) { + stream, err := binary.Download(ctx, part) + if err != nil { + t.Errorf("Download() failed: %v", err) + return nil, nil, err + } + output := make([]byte, 0) + rStream := stream.RecvStream() + for rStream.Advance() { + bytes := rStream.Value() + output = append(output, bytes...) + } + + if streamErr := rStream.Err(); streamErr != nil { + err := stream.Finish() + if err != nil { + t.Logf("Finish() failed: %v", err) + } + t.Logf("Advance() failed with: %v", streamErr) + return nil, streamErr, err + } + + if err := stream.Finish(); err != nil { + t.Logf("Finish() failed: %v", err) + return nil, nil, err + } + return output, nil, nil +} + +func prepDirectory(t *testing.T, rootDir string) { + path, perm := filepath.Join(rootDir, binarylib.VersionFile), os.FileMode(0600) + if err := ioutil.WriteFile(path, []byte(binarylib.Version), perm); err != nil { + t.Fatal(testutil.FormatLogLine(2, "WriteFile(%v, %v, %v) failed: %v", path, binarylib.Version, perm, err)) + } +} + +// testData creates up to 4MB of random bytes. +func testData(rg *testutil.Random) []byte { + size := rg.RandomIntn(1000 * binarylib.BufferLength) + data := rg.RandomBytes(size) + return data +} diff --git a/x/ref/services/internal/binarylib/v23_test.go b/x/ref/services/internal/binarylib/v23_test.go new file mode 100644 index 000000000..8b2d6457d --- /dev/null +++ b/x/ref/services/internal/binarylib/v23_test.go @@ -0,0 +1,15 @@ +// Copyright 2015 The Vanadium 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 binarylib_test + +import ( + "testing" + + "v.io/x/ref/test/v23test" +) + +func TestMain(m *testing.M) { + v23test.TestMain(m) +} diff --git a/x/ref/services/internal/packages/packages.go b/x/ref/services/internal/packages/packages.go new file mode 100644 index 000000000..15b253bff --- /dev/null +++ b/x/ref/services/internal/packages/packages.go @@ -0,0 +1,302 @@ +// Copyright 2015 The Vanadium 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 packages provides functionality to install ZIP and TAR packages. +package packages + +import ( + "archive/tar" + "archive/zip" + "compress/bzip2" + "compress/gzip" + "encoding/json" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "v.io/v23/services/repository" + "v.io/v23/verror" +) + +const ( + defaultType = "application/octet-stream" + createDirMode = 0755 + createFileMode = 0644 +) + +var typemap = map[string]repository.MediaInfo{ + ".zip": repository.MediaInfo{Type: "application/zip"}, + ".tar": repository.MediaInfo{Type: "application/x-tar"}, + ".tgz": repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"}, + ".tar.gz": repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"}, + ".tbz2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"}, + ".tb2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"}, + ".tbz": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"}, + ".tar.bz2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"}, +} + +const pkgPath = "v.io/x/ref/services/internal/packages" + +var ( + errBadMediaType = verror.Register(pkgPath+".errBadMediaType", verror.NoRetry, "{1:}{2:} unsupported media type{:_}") + errMkDirFailed = verror.Register(pkgPath+".errMkDirFailed", verror.NoRetry, "{1:}{2:} os.Mkdir({3}) failed{:_}") + errFailedToExtract = verror.Register(pkgPath+".errFailedToExtract", verror.NoRetry, "{1:}{2:} failed to extract file {3} outside of install directory{:_}") + errBadFileSize = verror.Register(pkgPath+".errBadFileSize", verror.NoRetry, "{1:}{2:} file size doesn't match for {3}: {4} != {5}{:_}") + errBadEncoding = verror.Register(pkgPath+".errBadEncoding", verror.NoRetry, "{1:}{2:} unsupported encoding{:_}") +) + +// MediaInfoFile returns the name of the file where the media info is stored for +// the given package file. +func MediaInfoFile(pkgFile string) string { + const mediaInfoFileSuffix = ".__info" + return pkgFile + mediaInfoFileSuffix +} + +// MediaInfoForFileName returns the MediaInfo based on the file's extension. +func MediaInfoForFileName(fileName string) repository.MediaInfo { + fileName = strings.ToLower(fileName) + for k, v := range typemap { + if strings.HasSuffix(fileName, k) { + return v + } + } + return repository.MediaInfo{Type: defaultType} +} + +func copyFile(src, dst string) error { + s, err := os.Open(src) + if err != nil { + return err + } + defer s.Close() + d, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, createFileMode) + if err != nil { + return err + } + defer d.Close() + if _, err = io.Copy(d, s); err != nil { + return err + } + return d.Sync() +} + +// Install installs a package in the given destination. If the package is a TAR +// or ZIP archive, the destination becomes a directory where the archive content +// is extracted. Otherwise, the destination is hard-linked to the package (or +// copied if hard link is not possible). +func Install(pkgFile, destination string) error { + mediaInfo, err := LoadMediaInfo(pkgFile) + if err != nil { + return err + } + switch mediaInfo.Type { + case "application/x-tar": + return extractTar(pkgFile, mediaInfo.Encoding, destination) + case "application/zip": + return extractZip(pkgFile, destination) + case defaultType, "text/plain": + if err := os.Link(pkgFile, destination); err != nil { + // Can't create hard link (e.g., different filesystem). + return copyFile(pkgFile, destination) + } + return nil + default: + // TODO(caprita): Instead of throwing an error, why not just + // handle things with os.Link(pkgFile, destination) as the two + // cases above? + return verror.New(errBadMediaType, nil, mediaInfo.Type) + } +} + +// LoadMediaInfo returns the MediaInfo for the given package file. +func LoadMediaInfo(pkgFile string) (repository.MediaInfo, error) { + jInfo, err := ioutil.ReadFile(MediaInfoFile(pkgFile)) + if err != nil { + return repository.MediaInfo{}, err + } + var info repository.MediaInfo + if err := json.Unmarshal(jInfo, &info); err != nil { + return repository.MediaInfo{}, err + } + return info, nil +} + +// SaveMediaInfo saves the media info for a package. +func SaveMediaInfo(pkgFile string, mediaInfo repository.MediaInfo) error { + jInfo, err := json.Marshal(mediaInfo) + if err != nil { + return err + } + infoFile := MediaInfoFile(pkgFile) + if err := ioutil.WriteFile(infoFile, jInfo, os.FileMode(0600)); err != nil { + return err + } + return nil +} + +// CreateZip creates a package from the files in the source directory. The +// created package is a Zip file. +func CreateZip(zipFile, sourceDir string) error { + z, err := os.OpenFile(zipFile, os.O_CREATE|os.O_WRONLY, os.FileMode(0644)) + if err != nil { + return err + } + defer z.Close() + w := zip.NewWriter(z) + if err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if sourceDir == path { + return nil + } + fh, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + fh.Method = zip.Deflate + fh.Name, _ = filepath.Rel(sourceDir, path) + hdr, err := w.CreateHeader(fh) + if err != nil { + return err + } + if !info.IsDir() { + content, err := ioutil.ReadFile(path) + if err != nil { + return err + } + if _, err = hdr.Write(content); err != nil { + return err + } + } + return nil + }); err != nil { + return err + } + if err := w.Close(); err != nil { + return err + } + if err := SaveMediaInfo(zipFile, repository.MediaInfo{Type: "application/zip"}); err != nil { + return err + } + return nil +} + +func extractZip(zipFile, installDir string) error { + if err := os.Mkdir(installDir, os.FileMode(createDirMode)); err != nil { + return verror.New(errMkDirFailed, nil, installDir, err) + } + zr, err := zip.OpenReader(zipFile) + if err != nil { + return err + } + for _, file := range zr.File { + fi := file.FileInfo() + name := filepath.Join(installDir, file.Name) + if !strings.HasPrefix(name, installDir) { + return verror.New(errFailedToExtract, nil, file.Name) + } + if fi.IsDir() { + if err := os.MkdirAll(name, os.FileMode(createDirMode)); err != nil && !os.IsExist(err) { + return err + } + continue + } + in, err := file.Open() + if err != nil { + return err + } + parentName := filepath.Dir(name) + if err := os.MkdirAll(parentName, os.FileMode(createDirMode)); err != nil { + return err + } + out, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, os.FileMode(createFileMode)) + if err != nil { + in.Close() + return err + } + nbytes, err := io.Copy(out, in) + in.Close() + out.Close() + if err != nil { + return err + } + if nbytes != fi.Size() { + return verror.New(errBadFileSize, nil, fi.Name(), nbytes, fi.Size()) + } + } + return nil +} + +func extractTar(pkgFile string, encoding string, installDir string) error { + if err := os.Mkdir(installDir, os.FileMode(createDirMode)); err != nil { + return verror.New(errMkDirFailed, nil, installDir, err) + } + f, err := os.Open(pkgFile) + if err != nil { + return err + } + defer f.Close() + + var reader io.Reader + switch encoding { + case "": + reader = f + case "gzip": + var err error + if reader, err = gzip.NewReader(f); err != nil { + return err + } + case "bzip2": + reader = bzip2.NewReader(f) + default: + return verror.New(errBadEncoding, nil, encoding) + } + + tr := tar.NewReader(reader) + for { + hdr, err := tr.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + name := filepath.Join(installDir, hdr.Name) + if !strings.HasPrefix(name, installDir) { + return verror.New(errFailedToExtract, nil, hdr.Name) + } + // Regular file + if hdr.Typeflag == tar.TypeReg { + parentName := filepath.Dir(name) + if err := os.MkdirAll(parentName, os.FileMode(createDirMode)); err != nil { + return err + } + out, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, os.FileMode(createFileMode)) + if err != nil { + return err + } + nbytes, err := io.Copy(out, tr) + out.Close() + if err != nil { + return err + } + if nbytes != hdr.Size { + return verror.New(errBadFileSize, nil, hdr.Name, nbytes, hdr.Size) + } + continue + } + // Directory + if hdr.Typeflag == tar.TypeDir { + if err := os.MkdirAll(name, os.FileMode(createDirMode)); err != nil && !os.IsExist(err) { + return err + } + continue + } + // Skip unsupported types + // TODO(rthellend): Consider adding support for Symlink. + } +} diff --git a/x/ref/services/internal/packages/packages_test.go b/x/ref/services/internal/packages/packages_test.go new file mode 100644 index 000000000..43b69a04e --- /dev/null +++ b/x/ref/services/internal/packages/packages_test.go @@ -0,0 +1,208 @@ +// Copyright 2015 The Vanadium 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 packages_test + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "sort" + "testing" + + "v.io/v23/services/repository" + + "v.io/x/ref/services/internal/packages" +) + +func TestInstall(t *testing.T) { + workdir, err := ioutil.TempDir("", "packages-test-") + if err != nil { + t.Fatalf("ioutil.TempDir failed: %v", err) + } + defer os.RemoveAll(workdir) + srcdir := filepath.Join(workdir, "src") + dstdir := filepath.Join(workdir, "dst") + createFiles(t, srcdir) + + zipfile := filepath.Join(workdir, "archivezip") + tarfile := filepath.Join(workdir, "archivetar") + tgzfile := filepath.Join(workdir, "archivetgz") + + makeZip(t, zipfile, srcdir) + makeTar(t, tarfile, srcdir) + doGzip(t, tarfile, tgzfile) + + binfile := filepath.Join(workdir, "binfile") + ioutil.WriteFile(binfile, []byte("This is a binary file"), os.FileMode(0644)) + ioutil.WriteFile(packages.MediaInfoFile(binfile), []byte(`{"type":"application/octet-stream"}`), os.FileMode(0644)) + + expected := []string{ + "a perm:700", + "a/b perm:700", + "a/b/xyzzy.txt perm:600", + "a/bar.txt perm:600", + "a/foo.txt perm:600", + } + for _, file := range []string{zipfile, tarfile, tgzfile} { + if err := packages.Install(file, dstdir); err != nil { + t.Errorf("packages.Install failed for %q: %v", file, err) + } + files := scanDir(dstdir) + if !reflect.DeepEqual(files, expected) { + t.Errorf("unexpected result for %q: got %q, want %q", file, files, expected) + } + if err := os.RemoveAll(dstdir); err != nil { + t.Fatalf("os.RemoveAll(%q) failed: %v", dstdir, err) + } + } + dstfile := filepath.Join(workdir, "dstfile") + if err := packages.Install(binfile, dstfile); err != nil { + t.Errorf("packages.Install failed for %q: %v", binfile, err) + } + contents, err := ioutil.ReadFile(dstfile) + if err != nil { + t.Errorf("ReadFile(%q) failed: %v", dstfile, err) + } + if want, got := "This is a binary file", string(contents); want != got { + t.Errorf("unexpected result for %q: got %q, want %q", binfile, got, want) + } +} + +func TestMediaInfo(t *testing.T) { + testcases := []struct { + filename string + expected repository.MediaInfo + }{ + {"foo.zip", repository.MediaInfo{Type: "application/zip"}}, + {"foo.ZIP", repository.MediaInfo{Type: "application/zip"}}, + {"foo.tar", repository.MediaInfo{Type: "application/x-tar"}}, + {"foo.TAR", repository.MediaInfo{Type: "application/x-tar"}}, + {"foo.tgz", repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"}}, + {"FOO.TAR.GZ", repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"}}, + {"foo.tbz2", repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"}}, + {"foo.tar.bz2", repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"}}, + {"foo", repository.MediaInfo{Type: "application/octet-stream"}}, + } + for _, tc := range testcases { + if got := packages.MediaInfoForFileName(tc.filename); !reflect.DeepEqual(got, tc.expected) { + t.Errorf("unexpected result for %q: got %v, want %v", tc.filename, got, tc.expected) + } + } +} + +func createFiles(t *testing.T, dir string) { + if err := os.Mkdir(dir, os.FileMode(0755)); err != nil { + t.Fatalf("os.Mkdir(%q) failed: %v", dir, err) + } + dirs := []string{"a", "a/b"} + for _, d := range dirs { + fullname := filepath.Join(dir, d) + if err := os.Mkdir(fullname, os.FileMode(0755)); err != nil { + t.Fatalf("os.Mkdir(%q) failed: %v", fullname, err) + } + } + files := []string{"a/foo.txt", "a/bar.txt", "a/b/xyzzy.txt"} + for _, f := range files { + fullname := filepath.Join(dir, f) + if err := ioutil.WriteFile(fullname, []byte(f), os.FileMode(0644)); err != nil { + t.Fatalf("ioutil.WriteFile(%q) failed: %v", fullname, err) + } + } +} + +func makeZip(t *testing.T, zipfile, dir string) { + if err := packages.CreateZip(zipfile, dir); err != nil { + t.Fatalf("packages.CreateZip failed: %v", err) + } +} + +func makeTar(t *testing.T, tarfile, dir string) { + tf, err := os.OpenFile(tarfile, os.O_CREATE|os.O_WRONLY, os.FileMode(0644)) + if err != nil { + t.Fatalf("os.OpenFile(%q) failed: %v", tarfile, err) + } + defer tf.Close() + + tw := tar.NewWriter(tf) + filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + t.Fatalf("Walk(%q) error: %v", dir, err) + } + if dir == path { + return nil + } + hdr, err := tar.FileInfoHeader(info, "") + if err != nil { + t.Fatalf("tar.FileInfoHeader failed: %v", err) + } + hdr.Name, _ = filepath.Rel(dir, path) + if err := tw.WriteHeader(hdr); err != nil { + t.Fatalf("tw.WriteHeader failed: %v", err) + } + if !info.IsDir() { + content, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("ioutil.ReadFile(%q) failed: %v", path, err) + } + if _, err := tw.Write(content); err != nil { + t.Fatalf("tw.Write failed: %v", err) + } + } + return nil + }) + if err := tw.Close(); err != nil { + t.Fatalf("tw.Close failed: %v", err) + } + if err := ioutil.WriteFile(packages.MediaInfoFile(tarfile), []byte(`{"type":"application/x-tar"}`), os.FileMode(0644)); err != nil { + t.Fatalf("ioutil.WriteFile() failed: %v", err) + } +} + +func doGzip(t *testing.T, infile, outfile string) { + in, err := os.Open(infile) + if err != nil { + t.Fatalf("os.Open(%q) failed: %v", infile, err) + } + defer in.Close() + out, err := os.OpenFile(outfile, os.O_CREATE|os.O_WRONLY, os.FileMode(0644)) + if err != nil { + t.Fatalf("os.OpenFile(%q) failed: %v", outfile, err) + } + defer out.Close() + writer := gzip.NewWriter(out) + defer writer.Close() + if _, err := io.Copy(writer, in); err != nil { + t.Fatalf("io.Copy() failed: %v", err) + } + + info, err := packages.LoadMediaInfo(infile) + if err != nil { + t.Fatalf("LoadMediaInfo(%q) failed: %v", infile, err) + } + info.Encoding = "gzip" + if err := packages.SaveMediaInfo(outfile, info); err != nil { + t.Fatalf("SaveMediaInfo(%v) failed: %v", outfile, err) + } +} + +func scanDir(root string) []string { + files := []string{} + filepath.Walk(root, func(path string, info os.FileInfo, _ error) error { + if root == path { + return nil + } + rel, _ := filepath.Rel(root, path) + perm := info.Mode() & 0700 + files = append(files, fmt.Sprintf("%s perm:%o", rel, perm)) + return nil + }) + sort.Strings(files) + return files +} diff --git a/x/ref/services/internal/profiles/listprofiles.go b/x/ref/services/internal/profiles/listprofiles.go new file mode 100644 index 000000000..66b6eb58f --- /dev/null +++ b/x/ref/services/internal/profiles/listprofiles.go @@ -0,0 +1,92 @@ +// Copyright 2015 The Vanadium 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 profiles + +import ( + // "bytes" + // "errors" + // "os/exec" + // "runtime" + // "strings" + + "v.io/v23/services/build" + // "v.io/v23/services/device" + "v.io/x/ref/services/profile" +) + +// GetKnownProfiles gets a list of description for all publicly known +// profiles. +// +// TODO(jsimsa): Avoid retrieving the list of known profiles from a +// remote server if a recent cached copy exists. +func GetKnownProfiles() ([]*profile.Specification, error) { + return []*profile.Specification{ + { + Label: "linux-amd64", + Description: "", + Arch: build.ArchitectureAmd64, + Os: build.OperatingSystemLinux, + Format: build.FormatElf, + }, + { + // Note that linux-386 is used instead of linux-x86 for the + // label to facilitate generation of a matching label string + // using the runtime.GOARCH value. In VDL, the 386 architecture + // is represented using the value X86 because the VDL grammar + // does not allow identifiers starting with a number. + Label: "linux-386", + Description: "", + Arch: build.ArchitectureX86, + Os: build.OperatingSystemLinux, + Format: build.FormatElf, + }, + { + Label: "linux-arm", + Description: "", + Arch: build.ArchitectureArm, + Os: build.OperatingSystemLinux, + Format: build.FormatElf, + }, + { + Label: "darwin-amd64", + Description: "", + Arch: build.ArchitectureAmd64, + Os: build.OperatingSystemDarwin, + Format: build.FormatMach, + }, + { + Label: "android-arm", + Description: "", + Arch: build.ArchitectureArm, + Os: build.OperatingSystemAndroid, + Format: build.FormatElf, + }, + }, nil + + // TODO(jsimsa): This function assumes the existence of a profile + // server from which a list of known profiles can be retrieved. The + // profile server is a work in progress. When it exists, the + // commented out code below should work. + + /* + knownProfiles := make([]profile.Specification, 0) + client, err := r.NewClient() + if err != nil { + return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("NewClient() failed: %v\n", err)) + } + defer client.Close() + server := // TODO + method := "List" + inputs := make([]interface{}, 0) + call, err := client.StartCall(server, method, inputs) + if err != nil { + return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("StartCall(%s, %q, %v) failed: %v\n", server, method, inputs, err)) + } + if err := call.Finish(&knownProfiles); err != nil { + return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("Finish(&knownProfile) failed: %v\n", err)) + } + return knownProfiles, nil + */ +} diff --git a/x/ref/services/profile/profile.vdl b/x/ref/services/profile/profile.vdl new file mode 100644 index 000000000..c2b91a559 --- /dev/null +++ b/x/ref/services/profile/profile.vdl @@ -0,0 +1,36 @@ +// Copyright 2015 The Vanadium 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 profile defines types for the implementation of Vanadium profiles. +package profile + +import "v.io/v23/services/build" + +// Library describes a shared library that applications may use. +type Library struct { + // Name is the name of the library. + Name string + // MajorVersion is the major version of the library. + MajorVersion string + // MinorVersion is the minor version of the library. + MinorVersion string +} + +// Specification is how we represent a profile internally. It should +// provide enough information to allow matching of binaries to devices. +type Specification struct { + // Label is a human-friendly concise label for the profile, + // e.g. "linux-media". + Label string + // Description is a human-friendly description of the profile. + Description string + // Arch is the target hardware architecture of the profile. + Arch build.Architecture + // Os is the target operating system of the profile. + Os build.OperatingSystem + // Format is the file format supported by the profile. + Format build.Format + // Libraries is a set of libraries the profile requires. + Libraries set[Library] +} diff --git a/x/ref/services/profile/profile.vdl.go b/x/ref/services/profile/profile.vdl.go new file mode 100644 index 000000000..26edcf820 --- /dev/null +++ b/x/ref/services/profile/profile.vdl.go @@ -0,0 +1,368 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated by the vanadium vdl tool. +// Package: profile + +// Package profile defines types for the implementation of Vanadium profiles. +package profile + +import ( + "v.io/v23/services/build" + "v.io/v23/vdl" +) + +var _ = __VDLInit() // Must be first; see __VDLInit comments for details. + +////////////////////////////////////////////////// +// Type definitions + +// Library describes a shared library that applications may use. +type Library struct { + // Name is the name of the library. + Name string + // MajorVersion is the major version of the library. + MajorVersion string + // MinorVersion is the minor version of the library. + MinorVersion string +} + +func (Library) VDLReflect(struct { + Name string `vdl:"v.io/x/ref/services/profile.Library"` +}) { +} + +func (x Library) VDLIsZero() bool { + return x == Library{} +} + +func (x Library) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_1); err != nil { + return err + } + if x.Name != "" { + if err := enc.NextFieldValueString(0, vdl.StringType, x.Name); err != nil { + return err + } + } + if x.MajorVersion != "" { + if err := enc.NextFieldValueString(1, vdl.StringType, x.MajorVersion); err != nil { + return err + } + } + if x.MinorVersion != "" { + if err := enc.NextFieldValueString(2, vdl.StringType, x.MinorVersion); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *Library) VDLRead(dec vdl.Decoder) error { + *x = Library{} + if err := dec.StartValue(__VDLType_struct_1); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_1 { + index = __VDLType_struct_1.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.Name = value + } + case 1: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.MajorVersion = value + } + case 2: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.MinorVersion = value + } + } + } +} + +// Specification is how we represent a profile internally. It should +// provide enough information to allow matching of binaries to devices. +type Specification struct { + // Label is a human-friendly concise label for the profile, + // e.g. "linux-media". + Label string + // Description is a human-friendly description of the profile. + Description string + // Arch is the target hardware architecture of the profile. + Arch build.Architecture + // Os is the target operating system of the profile. + Os build.OperatingSystem + // Format is the file format supported by the profile. + Format build.Format + // Libraries is a set of libraries the profile requires. + Libraries map[Library]struct{} +} + +func (Specification) VDLReflect(struct { + Name string `vdl:"v.io/x/ref/services/profile.Specification"` +}) { +} + +func (x Specification) VDLIsZero() bool { + if x.Label != "" { + return false + } + if x.Description != "" { + return false + } + if x.Arch != build.ArchitectureAmd64 { + return false + } + if x.Os != build.OperatingSystemDarwin { + return false + } + if x.Format != build.FormatElf { + return false + } + if len(x.Libraries) != 0 { + return false + } + return true +} + +func (x Specification) VDLWrite(enc vdl.Encoder) error { + if err := enc.StartValue(__VDLType_struct_2); err != nil { + return err + } + if x.Label != "" { + if err := enc.NextFieldValueString(0, vdl.StringType, x.Label); err != nil { + return err + } + } + if x.Description != "" { + if err := enc.NextFieldValueString(1, vdl.StringType, x.Description); err != nil { + return err + } + } + if x.Arch != build.ArchitectureAmd64 { + if err := enc.NextFieldValueString(2, __VDLType_enum_3, x.Arch.String()); err != nil { + return err + } + } + if x.Os != build.OperatingSystemDarwin { + if err := enc.NextFieldValueString(3, __VDLType_enum_4, x.Os.String()); err != nil { + return err + } + } + if x.Format != build.FormatElf { + if err := enc.NextFieldValueString(4, __VDLType_enum_5, x.Format.String()); err != nil { + return err + } + } + if len(x.Libraries) != 0 { + if err := enc.NextField(5); err != nil { + return err + } + if err := __VDLWriteAnon_set_1(enc, x.Libraries); err != nil { + return err + } + } + if err := enc.NextField(-1); err != nil { + return err + } + return enc.FinishValue() +} + +func __VDLWriteAnon_set_1(enc vdl.Encoder, x map[Library]struct{}) error { + if err := enc.StartValue(__VDLType_set_6); err != nil { + return err + } + if err := enc.SetLenHint(len(x)); err != nil { + return err + } + for key := range x { + if err := enc.NextEntry(false); err != nil { + return err + } + if err := key.VDLWrite(enc); err != nil { + return err + } + } + if err := enc.NextEntry(true); err != nil { + return err + } + return enc.FinishValue() +} + +func (x *Specification) VDLRead(dec vdl.Decoder) error { + *x = Specification{} + if err := dec.StartValue(__VDLType_struct_2); err != nil { + return err + } + decType := dec.Type() + for { + index, err := dec.NextField() + switch { + case err != nil: + return err + case index == -1: + return dec.FinishValue() + } + if decType != __VDLType_struct_2 { + index = __VDLType_struct_2.FieldIndexByName(decType.Field(index).Name) + if index == -1 { + if err := dec.SkipValue(); err != nil { + return err + } + continue + } + } + switch index { + case 0: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.Label = value + } + case 1: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + x.Description = value + } + case 2: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + if err := x.Arch.Set(value); err != nil { + return err + } + } + case 3: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + if err := x.Os.Set(value); err != nil { + return err + } + } + case 4: + switch value, err := dec.ReadValueString(); { + case err != nil: + return err + default: + if err := x.Format.Set(value); err != nil { + return err + } + } + case 5: + if err := __VDLReadAnon_set_1(dec, &x.Libraries); err != nil { + return err + } + } + } +} + +func __VDLReadAnon_set_1(dec vdl.Decoder, x *map[Library]struct{}) error { + if err := dec.StartValue(__VDLType_set_6); err != nil { + return err + } + var tmpMap map[Library]struct{} + if len := dec.LenHint(); len > 0 { + tmpMap = make(map[Library]struct{}, len) + } + for { + switch done, err := dec.NextEntry(); { + case err != nil: + return err + case done: + *x = tmpMap + return dec.FinishValue() + default: + var key Library + if err := key.VDLRead(dec); err != nil { + return err + } + if tmpMap == nil { + tmpMap = make(map[Library]struct{}) + } + tmpMap[key] = struct{}{} + } + } +} + +// Hold type definitions in package-level variables, for better performance. +var ( + __VDLType_struct_1 *vdl.Type + __VDLType_struct_2 *vdl.Type + __VDLType_enum_3 *vdl.Type + __VDLType_enum_4 *vdl.Type + __VDLType_enum_5 *vdl.Type + __VDLType_set_6 *vdl.Type +) + +var __VDLInitCalled bool + +// __VDLInit performs vdl initialization. It is safe to call multiple times. +// If you have an init ordering issue, just insert the following line verbatim +// into your source files in this package, right after the "package foo" clause: +// +// var _ = __VDLInit() +// +// The purpose of this function is to ensure that vdl initialization occurs in +// the right order, and very early in the init sequence. In particular, vdl +// registration and package variable initialization needs to occur before +// functions like vdl.TypeOf will work properly. +// +// This function returns a dummy value, so that it can be used to initialize the +// first var in the file, to take advantage of Go's defined init order. +func __VDLInit() struct{} { + if __VDLInitCalled { + return struct{}{} + } + __VDLInitCalled = true + + // Register types. + vdl.Register((*Library)(nil)) + vdl.Register((*Specification)(nil)) + + // Initialize type definitions. + __VDLType_struct_1 = vdl.TypeOf((*Library)(nil)).Elem() + __VDLType_struct_2 = vdl.TypeOf((*Specification)(nil)).Elem() + __VDLType_enum_3 = vdl.TypeOf((*build.Architecture)(nil)) + __VDLType_enum_4 = vdl.TypeOf((*build.OperatingSystem)(nil)) + __VDLType_enum_5 = vdl.TypeOf((*build.Format)(nil)) + __VDLType_set_6 = vdl.TypeOf((*map[Library]struct{})(nil)) + + return struct{}{} +} diff --git a/x/ref/services/repository/repository.vdl b/x/ref/services/repository/repository.vdl new file mode 100644 index 000000000..a9f62872a --- /dev/null +++ b/x/ref/services/repository/repository.vdl @@ -0,0 +1,58 @@ +// Copyright 2015 The Vanadium 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 repository augments the v.io/v23/services/repository interfaces with +// implementation-specific configuration methods. +package repository + +import ( + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/x/ref/services/profile" + public "v.io/v23/services/repository" +) + +// Application describes an application repository internally. Besides the +// public Application interface, it allows adding and removing application +// envelopes, as well as querying for a list of supported profiles. +type Application interface { + public.Application + // Put adds the given application envelope for the given profile and + // application version (required, and specified through the object name + // suffix). + // + // An error is returned if an envelope already exists, unless the + // overwrite option is set. + Put(Profile string, Envelope application.Envelope, Overwrite bool) error {access.Write} + // Remove removes the application envelope for the given profile + // name and application version (specified through the object name + // suffix). + // + // If no version is specified as part of the suffix, the method removes + // all versions for the given profile. + // + // If the profile is the string "*", all profiles are removed for the + // given version (or for all versions if the version is not specified). + Remove(Profile string) error {access.Write} + // Profiles returns the supported profiles for the application version + // specified through the object name suffix. If the version is not + // specified, Profiles returns the union of profiles across all + // versions. + Profiles() ([]string | error) {access.Read} +} + +// Profile describes a profile internally. Besides the public Profile +// interface, it allows to add and remove profile specifications. +type Profile interface { + public.Profile + // Specification returns the profile specification for the profile + // identified through the object name suffix. + Specification() (profile.Specification | error) {access.Read} + // Put sets the profile specification for the profile identified + // through the object name suffix. + Put(Specification profile.Specification) error {access.Write} + // Remove removes the profile specification for the profile + // identified through the object name suffix. + Remove() error {access.Write} +} diff --git a/x/ref/services/repository/repository.vdl.go b/x/ref/services/repository/repository.vdl.go new file mode 100644 index 000000000..9a4152799 --- /dev/null +++ b/x/ref/services/repository/repository.vdl.go @@ -0,0 +1,435 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated by the vanadium vdl tool. +// Package: repository + +// Package repository augments the v.io/v23/services/repository interfaces with +// implementation-specific configuration methods. +package repository + +import ( + "v.io/v23" + "v.io/v23/context" + "v.io/v23/rpc" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/services/permissions" + "v.io/v23/services/repository" + "v.io/v23/services/tidyable" + "v.io/v23/vdl" + "v.io/x/ref/services/profile" +) + +var _ = __VDLInit() // Must be first; see __VDLInit comments for details. + +////////////////////////////////////////////////// +// Interface definitions + +// ApplicationClientMethods is the client interface +// containing Application methods. +// +// Application describes an application repository internally. Besides the +// public Application interface, it allows adding and removing application +// envelopes, as well as querying for a list of supported profiles. +type ApplicationClientMethods interface { + // Application provides access to application envelopes. An + // application envelope is identified by an application name and an + // application version, which are specified through the object name, + // and a profile name, which is specified using a method argument. + // + // Example: + // /apps/search/v1.Match([]string{"base", "media"}) + // returns an application envelope that can be used for downloading + // and executing the "search" application, version "v1", runnable + // on either the "base" or "media" profile. + repository.ApplicationClientMethods + // Put adds the given application envelope for the given profile and + // application version (required, and specified through the object name + // suffix). + // + // An error is returned if an envelope already exists, unless the + // overwrite option is set. + Put(_ *context.T, Profile string, Envelope application.Envelope, Overwrite bool, _ ...rpc.CallOpt) error + // Remove removes the application envelope for the given profile + // name and application version (specified through the object name + // suffix). + // + // If no version is specified as part of the suffix, the method removes + // all versions for the given profile. + // + // If the profile is the string "*", all profiles are removed for the + // given version (or for all versions if the version is not specified). + Remove(_ *context.T, Profile string, _ ...rpc.CallOpt) error + // Profiles returns the supported profiles for the application version + // specified through the object name suffix. If the version is not + // specified, Profiles returns the union of profiles across all + // versions. + Profiles(*context.T, ...rpc.CallOpt) ([]string, error) +} + +// ApplicationClientStub adds universal methods to ApplicationClientMethods. +type ApplicationClientStub interface { + ApplicationClientMethods + rpc.UniversalServiceMethods +} + +// ApplicationClient returns a client stub for Application. +func ApplicationClient(name string) ApplicationClientStub { + return implApplicationClientStub{name, repository.ApplicationClient(name)} +} + +type implApplicationClientStub struct { + name string + + repository.ApplicationClientStub +} + +func (c implApplicationClientStub) Put(ctx *context.T, i0 string, i1 application.Envelope, i2 bool, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Put", []interface{}{i0, i1, i2}, nil, opts...) + return +} + +func (c implApplicationClientStub) Remove(ctx *context.T, i0 string, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Remove", []interface{}{i0}, nil, opts...) + return +} + +func (c implApplicationClientStub) Profiles(ctx *context.T, opts ...rpc.CallOpt) (o0 []string, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Profiles", nil, []interface{}{&o0}, opts...) + return +} + +// ApplicationServerMethods is the interface a server writer +// implements for Application. +// +// Application describes an application repository internally. Besides the +// public Application interface, it allows adding and removing application +// envelopes, as well as querying for a list of supported profiles. +type ApplicationServerMethods interface { + // Application provides access to application envelopes. An + // application envelope is identified by an application name and an + // application version, which are specified through the object name, + // and a profile name, which is specified using a method argument. + // + // Example: + // /apps/search/v1.Match([]string{"base", "media"}) + // returns an application envelope that can be used for downloading + // and executing the "search" application, version "v1", runnable + // on either the "base" or "media" profile. + repository.ApplicationServerMethods + // Put adds the given application envelope for the given profile and + // application version (required, and specified through the object name + // suffix). + // + // An error is returned if an envelope already exists, unless the + // overwrite option is set. + Put(_ *context.T, _ rpc.ServerCall, Profile string, Envelope application.Envelope, Overwrite bool) error + // Remove removes the application envelope for the given profile + // name and application version (specified through the object name + // suffix). + // + // If no version is specified as part of the suffix, the method removes + // all versions for the given profile. + // + // If the profile is the string "*", all profiles are removed for the + // given version (or for all versions if the version is not specified). + Remove(_ *context.T, _ rpc.ServerCall, Profile string) error + // Profiles returns the supported profiles for the application version + // specified through the object name suffix. If the version is not + // specified, Profiles returns the union of profiles across all + // versions. + Profiles(*context.T, rpc.ServerCall) ([]string, error) +} + +// ApplicationServerStubMethods is the server interface containing +// Application methods, as expected by rpc.Server. +// There is no difference between this interface and ApplicationServerMethods +// since there are no streaming methods. +type ApplicationServerStubMethods ApplicationServerMethods + +// ApplicationServerStub adds universal methods to ApplicationServerStubMethods. +type ApplicationServerStub interface { + ApplicationServerStubMethods + // Describe the Application interfaces. + Describe__() []rpc.InterfaceDesc +} + +// ApplicationServer returns a server stub for Application. +// It converts an implementation of ApplicationServerMethods into +// an object that may be used by rpc.Server. +func ApplicationServer(impl ApplicationServerMethods) ApplicationServerStub { + stub := implApplicationServerStub{ + impl: impl, + ApplicationServerStub: repository.ApplicationServer(impl), + } + // Initialize GlobState; always check the stub itself first, to handle the + // case where the user has the Glob method defined in their VDL source. + if gs := rpc.NewGlobState(stub); gs != nil { + stub.gs = gs + } else if gs := rpc.NewGlobState(impl); gs != nil { + stub.gs = gs + } + return stub +} + +type implApplicationServerStub struct { + impl ApplicationServerMethods + repository.ApplicationServerStub + gs *rpc.GlobState +} + +func (s implApplicationServerStub) Put(ctx *context.T, call rpc.ServerCall, i0 string, i1 application.Envelope, i2 bool) error { + return s.impl.Put(ctx, call, i0, i1, i2) +} + +func (s implApplicationServerStub) Remove(ctx *context.T, call rpc.ServerCall, i0 string) error { + return s.impl.Remove(ctx, call, i0) +} + +func (s implApplicationServerStub) Profiles(ctx *context.T, call rpc.ServerCall) ([]string, error) { + return s.impl.Profiles(ctx, call) +} + +func (s implApplicationServerStub) Globber() *rpc.GlobState { + return s.gs +} + +func (s implApplicationServerStub) Describe__() []rpc.InterfaceDesc { + return []rpc.InterfaceDesc{ApplicationDesc, repository.ApplicationDesc, permissions.ObjectDesc, tidyable.TidyableDesc} +} + +// ApplicationDesc describes the Application interface. +var ApplicationDesc rpc.InterfaceDesc = descApplication + +// descApplication hides the desc to keep godoc clean. +var descApplication = rpc.InterfaceDesc{ + Name: "Application", + PkgPath: "v.io/x/ref/services/repository", + Doc: "// Application describes an application repository internally. Besides the\n// public Application interface, it allows adding and removing application\n// envelopes, as well as querying for a list of supported profiles.", + Embeds: []rpc.EmbedDesc{ + {"Application", "v.io/v23/services/repository", "// Application provides access to application envelopes. An\n// application envelope is identified by an application name and an\n// application version, which are specified through the object name,\n// and a profile name, which is specified using a method argument.\n//\n// Example:\n// /apps/search/v1.Match([]string{\"base\", \"media\"})\n// returns an application envelope that can be used for downloading\n// and executing the \"search\" application, version \"v1\", runnable\n// on either the \"base\" or \"media\" profile."}, + }, + Methods: []rpc.MethodDesc{ + { + Name: "Put", + Doc: "// Put adds the given application envelope for the given profile and\n// application version (required, and specified through the object name\n// suffix).\n//\n// An error is returned if an envelope already exists, unless the\n// overwrite option is set.", + InArgs: []rpc.ArgDesc{ + {"Profile", ``}, // string + {"Envelope", ``}, // application.Envelope + {"Overwrite", ``}, // bool + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))}, + }, + { + Name: "Remove", + Doc: "// Remove removes the application envelope for the given profile\n// name and application version (specified through the object name\n// suffix).\n//\n// If no version is specified as part of the suffix, the method removes\n// all versions for the given profile.\n//\n// If the profile is the string \"*\", all profiles are removed for the\n// given version (or for all versions if the version is not specified).", + InArgs: []rpc.ArgDesc{ + {"Profile", ``}, // string + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))}, + }, + { + Name: "Profiles", + Doc: "// Profiles returns the supported profiles for the application version\n// specified through the object name suffix. If the version is not\n// specified, Profiles returns the union of profiles across all\n// versions.", + OutArgs: []rpc.ArgDesc{ + {"", ``}, // []string + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))}, + }, + }, +} + +// ProfileClientMethods is the client interface +// containing Profile methods. +// +// Profile describes a profile internally. Besides the public Profile +// interface, it allows to add and remove profile specifications. +type ProfileClientMethods interface { + // Profile abstracts a device's ability to run binaries, and hides + // specifics such as the operating system, hardware architecture, and + // the set of installed libraries. Profiles describe binaries and + // devices, and are used to match them. + repository.ProfileClientMethods + // Specification returns the profile specification for the profile + // identified through the object name suffix. + Specification(*context.T, ...rpc.CallOpt) (profile.Specification, error) + // Put sets the profile specification for the profile identified + // through the object name suffix. + Put(_ *context.T, Specification profile.Specification, _ ...rpc.CallOpt) error + // Remove removes the profile specification for the profile + // identified through the object name suffix. + Remove(*context.T, ...rpc.CallOpt) error +} + +// ProfileClientStub adds universal methods to ProfileClientMethods. +type ProfileClientStub interface { + ProfileClientMethods + rpc.UniversalServiceMethods +} + +// ProfileClient returns a client stub for Profile. +func ProfileClient(name string) ProfileClientStub { + return implProfileClientStub{name, repository.ProfileClient(name)} +} + +type implProfileClientStub struct { + name string + + repository.ProfileClientStub +} + +func (c implProfileClientStub) Specification(ctx *context.T, opts ...rpc.CallOpt) (o0 profile.Specification, err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Specification", nil, []interface{}{&o0}, opts...) + return +} + +func (c implProfileClientStub) Put(ctx *context.T, i0 profile.Specification, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Put", []interface{}{i0}, nil, opts...) + return +} + +func (c implProfileClientStub) Remove(ctx *context.T, opts ...rpc.CallOpt) (err error) { + err = v23.GetClient(ctx).Call(ctx, c.name, "Remove", nil, nil, opts...) + return +} + +// ProfileServerMethods is the interface a server writer +// implements for Profile. +// +// Profile describes a profile internally. Besides the public Profile +// interface, it allows to add and remove profile specifications. +type ProfileServerMethods interface { + // Profile abstracts a device's ability to run binaries, and hides + // specifics such as the operating system, hardware architecture, and + // the set of installed libraries. Profiles describe binaries and + // devices, and are used to match them. + repository.ProfileServerMethods + // Specification returns the profile specification for the profile + // identified through the object name suffix. + Specification(*context.T, rpc.ServerCall) (profile.Specification, error) + // Put sets the profile specification for the profile identified + // through the object name suffix. + Put(_ *context.T, _ rpc.ServerCall, Specification profile.Specification) error + // Remove removes the profile specification for the profile + // identified through the object name suffix. + Remove(*context.T, rpc.ServerCall) error +} + +// ProfileServerStubMethods is the server interface containing +// Profile methods, as expected by rpc.Server. +// There is no difference between this interface and ProfileServerMethods +// since there are no streaming methods. +type ProfileServerStubMethods ProfileServerMethods + +// ProfileServerStub adds universal methods to ProfileServerStubMethods. +type ProfileServerStub interface { + ProfileServerStubMethods + // Describe the Profile interfaces. + Describe__() []rpc.InterfaceDesc +} + +// ProfileServer returns a server stub for Profile. +// It converts an implementation of ProfileServerMethods into +// an object that may be used by rpc.Server. +func ProfileServer(impl ProfileServerMethods) ProfileServerStub { + stub := implProfileServerStub{ + impl: impl, + ProfileServerStub: repository.ProfileServer(impl), + } + // Initialize GlobState; always check the stub itself first, to handle the + // case where the user has the Glob method defined in their VDL source. + if gs := rpc.NewGlobState(stub); gs != nil { + stub.gs = gs + } else if gs := rpc.NewGlobState(impl); gs != nil { + stub.gs = gs + } + return stub +} + +type implProfileServerStub struct { + impl ProfileServerMethods + repository.ProfileServerStub + gs *rpc.GlobState +} + +func (s implProfileServerStub) Specification(ctx *context.T, call rpc.ServerCall) (profile.Specification, error) { + return s.impl.Specification(ctx, call) +} + +func (s implProfileServerStub) Put(ctx *context.T, call rpc.ServerCall, i0 profile.Specification) error { + return s.impl.Put(ctx, call, i0) +} + +func (s implProfileServerStub) Remove(ctx *context.T, call rpc.ServerCall) error { + return s.impl.Remove(ctx, call) +} + +func (s implProfileServerStub) Globber() *rpc.GlobState { + return s.gs +} + +func (s implProfileServerStub) Describe__() []rpc.InterfaceDesc { + return []rpc.InterfaceDesc{ProfileDesc, repository.ProfileDesc} +} + +// ProfileDesc describes the Profile interface. +var ProfileDesc rpc.InterfaceDesc = descProfile + +// descProfile hides the desc to keep godoc clean. +var descProfile = rpc.InterfaceDesc{ + Name: "Profile", + PkgPath: "v.io/x/ref/services/repository", + Doc: "// Profile describes a profile internally. Besides the public Profile\n// interface, it allows to add and remove profile specifications.", + Embeds: []rpc.EmbedDesc{ + {"Profile", "v.io/v23/services/repository", "// Profile abstracts a device's ability to run binaries, and hides\n// specifics such as the operating system, hardware architecture, and\n// the set of installed libraries. Profiles describe binaries and\n// devices, and are used to match them."}, + }, + Methods: []rpc.MethodDesc{ + { + Name: "Specification", + Doc: "// Specification returns the profile specification for the profile\n// identified through the object name suffix.", + OutArgs: []rpc.ArgDesc{ + {"", ``}, // profile.Specification + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Read"))}, + }, + { + Name: "Put", + Doc: "// Put sets the profile specification for the profile identified\n// through the object name suffix.", + InArgs: []rpc.ArgDesc{ + {"Specification", ``}, // profile.Specification + }, + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))}, + }, + { + Name: "Remove", + Doc: "// Remove removes the profile specification for the profile\n// identified through the object name suffix.", + Tags: []*vdl.Value{vdl.ValueOf(access.Tag("Write"))}, + }, + }, +} + +var __VDLInitCalled bool + +// __VDLInit performs vdl initialization. It is safe to call multiple times. +// If you have an init ordering issue, just insert the following line verbatim +// into your source files in this package, right after the "package foo" clause: +// +// var _ = __VDLInit() +// +// The purpose of this function is to ensure that vdl initialization occurs in +// the right order, and very early in the init sequence. In particular, vdl +// registration and package variable initialization needs to occur before +// functions like vdl.TypeOf will work properly. +// +// This function returns a dummy value, so that it can be used to initialize the +// first var in the file, to take advantage of Go's defined init order. +func __VDLInit() struct{} { + if __VDLInitCalled { + return struct{}{} + } + __VDLInitCalled = true + + return struct{}{} +} From 318f5118e6d04103e11b41c131178281f2ec9ed0 Mon Sep 17 00:00:00 2001 From: Razvan Musaloiu-E <razvanm@google.com> Date: Wed, 2 Jan 2019 18:52:39 -0800 Subject: [PATCH 2/5] Fix the tests --- x/ref/services/device/claimable/claimable_v23_test.go | 5 +++++ x/ref/services/device/device/acl_test.go | 8 +++++++- .../internal/impl/applife/instance_reaping_test.go | 6 ++++++ .../impl/daemonreap/instance_reaping_kill_test.go | 6 ++++++ .../internal/impl/globsuid/signature_match_test.go | 8 +++++++- x/ref/services/device/deviced/internal/impl/impl_test.go | 6 ++++++ .../deviced/internal/impl/perms/debug_perms_test.go | 6 ++++++ 7 files changed, 43 insertions(+), 2 deletions(-) diff --git a/x/ref/services/device/claimable/claimable_v23_test.go b/x/ref/services/device/claimable/claimable_v23_test.go index 579aeb5ac..9fbf46867 100644 --- a/x/ref/services/device/claimable/claimable_v23_test.go +++ b/x/ref/services/device/claimable/claimable_v23_test.go @@ -12,9 +12,14 @@ import ( "testing" libsec "v.io/x/ref/lib/security" + "v.io/x/ref/runtime/factories/library" "v.io/x/ref/test/v23test" ) +func init() { + // Allow v23.Init to be called multiple times. + library.AllowMultipleInitializations = true +} func TestV23ClaimableServer(t *testing.T) { v23test.SkipUnlessRunningIntegrationTests(t) sh := v23test.NewShell(t, nil) diff --git a/x/ref/services/device/device/acl_test.go b/x/ref/services/device/device/acl_test.go index 6b19ba251..fa8190dbc 100644 --- a/x/ref/services/device/device/acl_test.go +++ b/x/ref/services/device/device/acl_test.go @@ -11,18 +11,24 @@ import ( "strings" "testing" - "v.io/v23" + v23 "v.io/v23" "v.io/v23/security" "v.io/v23/security/access" "v.io/v23/verror" "v.io/x/lib/cmdline" "v.io/x/ref/lib/v23cmd" + "v.io/x/ref/runtime/factories/library" "v.io/x/ref/test" cmd_device "v.io/x/ref/services/device/device" "v.io/x/ref/services/internal/servicetest" ) +func init() { + // Allow v23.Init to be called multiple times. + library.AllowMultipleInitializations = true +} + const pkgPath = "v.io/x/ref/services/device/main" var ( diff --git a/x/ref/services/device/deviced/internal/impl/applife/instance_reaping_test.go b/x/ref/services/device/deviced/internal/impl/applife/instance_reaping_test.go index 9a26445db..3845ca4ee 100644 --- a/x/ref/services/device/deviced/internal/impl/applife/instance_reaping_test.go +++ b/x/ref/services/device/deviced/internal/impl/applife/instance_reaping_test.go @@ -14,9 +14,15 @@ import ( "v.io/v23/services/stats" "v.io/v23/vdl" + "v.io/x/ref/runtime/factories/library" "v.io/x/ref/services/device/deviced/internal/impl/utiltest" ) +func init() { + // Allow v23.Init to be called multiple times. + library.AllowMultipleInitializations = true +} + func TestReaperNoticesAppDeath(t *testing.T) { cleanup, ctx, sh, envelope, root, helperPath, _ := utiltest.StartupHelper(t) defer cleanup() diff --git a/x/ref/services/device/deviced/internal/impl/daemonreap/instance_reaping_kill_test.go b/x/ref/services/device/deviced/internal/impl/daemonreap/instance_reaping_kill_test.go index e727e51ab..b272bc3c6 100644 --- a/x/ref/services/device/deviced/internal/impl/daemonreap/instance_reaping_kill_test.go +++ b/x/ref/services/device/deviced/internal/impl/daemonreap/instance_reaping_kill_test.go @@ -11,9 +11,15 @@ import ( "v.io/v23/services/device" "v.io/x/ref" + "v.io/x/ref/runtime/factories/library" "v.io/x/ref/services/device/deviced/internal/impl/utiltest" ) +func init() { + // Allow v23.Init to be called multiple times. + library.AllowMultipleInitializations = true +} + func TestReapReconciliationViaKill(t *testing.T) { cleanup, ctx, sh, envelope, root, helperPath, _ := utiltest.StartupHelper(t) defer cleanup() diff --git a/x/ref/services/device/deviced/internal/impl/globsuid/signature_match_test.go b/x/ref/services/device/deviced/internal/impl/globsuid/signature_match_test.go index e6780be03..50ea4debc 100644 --- a/x/ref/services/device/deviced/internal/impl/globsuid/signature_match_test.go +++ b/x/ref/services/device/deviced/internal/impl/globsuid/signature_match_test.go @@ -10,13 +10,14 @@ import ( "path/filepath" "testing" - "v.io/v23" + v23 "v.io/v23" "v.io/v23/naming" "v.io/v23/security" "v.io/v23/services/application" "v.io/v23/services/device" "v.io/v23/services/repository" "v.io/v23/verror" + "v.io/x/ref/runtime/factories/library" "v.io/x/ref/services/device/deviced/internal/impl/utiltest" "v.io/x/ref/services/device/deviced/internal/versioning" "v.io/x/ref/services/device/internal/errors" @@ -26,6 +27,11 @@ import ( "v.io/x/ref/test/testutil" ) +func init() { + // Allow v23.Init to be called multiple times. + library.AllowMultipleInitializations = true +} + func TestDownloadSignatureMatch(t *testing.T) { ctx, shutdown := test.V23Init() defer shutdown() diff --git a/x/ref/services/device/deviced/internal/impl/impl_test.go b/x/ref/services/device/deviced/internal/impl/impl_test.go index 4ede95256..39c849e25 100644 --- a/x/ref/services/device/deviced/internal/impl/impl_test.go +++ b/x/ref/services/device/deviced/internal/impl/impl_test.go @@ -32,8 +32,14 @@ import ( "v.io/x/ref/test" "v.io/x/ref/test/expect" "v.io/x/ref/test/testutil" + "v.io/x/ref/runtime/factories/library" ) +func init() { + // Allow v23.Init to be called multiple times. + library.AllowMultipleInitializations = true +} + func TestMain(m *testing.M) { utiltest.TestMainImpl(m) } diff --git a/x/ref/services/device/deviced/internal/impl/perms/debug_perms_test.go b/x/ref/services/device/deviced/internal/impl/perms/debug_perms_test.go index bb9571803..4e07819ca 100644 --- a/x/ref/services/device/deviced/internal/impl/perms/debug_perms_test.go +++ b/x/ref/services/device/deviced/internal/impl/perms/debug_perms_test.go @@ -16,10 +16,16 @@ import ( "v.io/v23/services/permissions" "v.io/v23/verror" + "v.io/x/ref/runtime/factories/library" "v.io/x/ref/services/device/deviced/internal/impl/utiltest" "v.io/x/ref/test/testutil" ) +func init() { + // Allow v23.Init to be called multiple times. + library.AllowMultipleInitializations = true +} + func updateAccessList(t *testing.T, ctx *context.T, blessing, right string, name ...string) { accessStub := permissions.ObjectClient(naming.Join(name...)) perms, version, err := accessStub.GetPermissions(ctx) From aa4beb373b28bdb0fb72ae6b8624cb08096fff50 Mon Sep 17 00:00:00 2001 From: Razvan Musaloiu-E <razvanm@google.com> Date: Fri, 4 Jan 2019 10:05:47 -0800 Subject: [PATCH 3/5] Add the devicex script This script works fine. I did not initially included because I was not sure it's relevant. --- x/ref/services/device/devicex | 427 ++++++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100755 x/ref/services/device/devicex diff --git a/x/ref/services/device/devicex b/x/ref/services/device/devicex new file mode 100755 index 000000000..aede24059 --- /dev/null +++ b/x/ref/services/device/devicex @@ -0,0 +1,427 @@ +#!/bin/bash +# Copyright 2015 The Vanadium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +# +# Administers a device manager installation. +# +# This script is a thin wrapper on top of the deviced commands. Its main +# purpose is to set up the installation by fetching the binaries required for a +# device manager installation from a few possible sources and setting up the +# setuid helper. + +set -e + +usage() { + echo "usage:" + echo + echo "Install device manager:" + echo "V23_DEVICE_DIR=<installation dir> ./devicex install [<binary source>] [ args for installer... ] [ -- args for device manager...]" + echo " Possible values for <binary source>:" + echo " unspecified: get binaries from local repository" + echo " /path/to/binaries: get binaries from local filesystem" + echo " http://host/path: get binaries from HTTP server" + echo + echo "Uninstall device manager:" + echo "V23_DEVICE_DIR=<installation dir> ./devicex uninstall" + echo + echo "Start device manager:" + echo "V23_DEVICE_DIR=<installation dir> ./devicex start" + echo + echo "Stop device manager:" + echo "V23_DEVICE_DIR=<installation dir> ./devicex stop" + echo "V23_DEVICE_DIR should be 0711 when running in multi-user" + echo "mode and all of its parents directories need to be at least" + echo "0511." +} + +############################################################################### +# Wrapper around chown that works differently on Mac and Linux +# Arguments: +# arguments to chown command +# Returns: +# None +############################################################################### +portable_chown() { + case "$(uname)" in + "Darwin") + sudo /usr/sbin/chown "$@" + ;; + "Linux") + sudo chown "$@" + ;; + esac +} + +############################################################################### +# Sets up the target to be owned by root with the suid bit on. +# Arguments: +# path to target +# Returns: +# None +############################################################################### +make_suid() { + local -r target="$1" + local root_group="root" + if [[ "$(uname)" == "Darwin" ]]; then + # Group root not available on Darwin. + root_group="wheel" + fi + portable_chown "root:${root_group}" "${target}" + sudo chmod 4551 "${target}" +} + +############################################################################### +# Runs a command as the device manager user. Assumes V23_DEVICE_DIR exists +# and gets the device manager user from the owner of that directory. +# Globals: +# V23_DEVICE_DIR +# Arguments: +# command to run and its arguments +# Returns: +# None +############################################################################### +run() { + local -r devmgr_user=$(getdevowner) + if [[ "${devmgr_user}" == $(whoami) ]]; then + "$@" + elif [[ "$(uname)" == "Darwin" ]]; then + # We use su -u on Darwin because Darwin su is different from Linux su + # and is not found in GCE or EC2 images. + sudo -u "${devmgr_user}" \ + V23_NAMESPACE="${V23_NAMESPACE}" \ + V23_DEVICE_DIR="${V23_DEVICE_DIR}" \ + "$@" + else + # We use sudo/su rather than just sudo -u because the latter is often + # set up to require a password in common GCE and EC2 images. + sudo V23_NAMESPACE="${V23_NAMESPACE}" V23_DEVICE_DIR="${V23_DEVICE_DIR}" \ + su "${devmgr_user}" -s /bin/bash -c \ + "$*" + fi +} + +############################################################################### +# Copies one binary from source to destination. +# Arguments: +# name of the binary +# source dir of binary +# destination dir of binary +# Returns: +# None +############################################################################### +copy_binary() { + local -r BIN_NAME="$1" + local -r BIN_SRC_DIR="$2" + local -r BIN_DEST_DIR="$3" + local -r SOURCE="${BIN_SRC_DIR}/${BIN_NAME}" + if [[ -x "${SOURCE}" ]]; then + local -r DESTINATION="${BIN_DEST_DIR}/${BIN_NAME}" + cp "${SOURCE}" "${DESTINATION}" + chmod 700 "${DESTINATION}" + else + echo "couldn't find ${SOURCE}" + exit 1 + fi +} + +############################################################################### +# Guesses if the argument is a url. +# Arguments: +# potential url +# Returns: +# 0 if the argument looks like a url, 1 otherwise +############################################################################### +urlmatch() { + case "$1" in + http://*) return 0;; + https://*) return 0;; + ftp://*) return 0;; + file://*) return 0;; + *) return 1;; + esac +} +############################################################################### +# Fetches binaries needed by device manager installation. +# Globals: +# BIN_NAMES +# JIRI_ROOT +# Arguments: +# destination for binaries +# source of binaries +# Returns: +# None +############################################################################### +get_binaries() { + local -r BIN_INSTALL="$1" + local -r BIN_SOURCE="$2" + + local bin_names_str="" + for bin_name in ${BIN_NAMES}; do + bin_names_str+=" ${bin_name}" + done + + # If source is not specified, try to look for it in the repository. + if [[ -z "${BIN_SOURCE}" ]]; then + if [[ -z "${JIRI_ROOT}" ]]; then + echo 'ERROR: binary source not specified and no local repository available' + exit 1 + fi + local -r REPO_BIN_DIR="${JIRI_ROOT}/release/go/bin" + echo "Fetching binaries:${bin_names_str} from build repository: ${REPO_BIN_DIR} ..." + for bin_name in ${BIN_NAMES}; do + copy_binary "${bin_name}" "${REPO_BIN_DIR}" "${BIN_INSTALL}" + done + return + fi + + # If the source is specified as an existing local filesystem path, + # look for the binaries there. + if [[ -d "${BIN_SOURCE}" ]]; then + echo "Fetching binaries:${bin_names_str} locally from: ${BIN_SOURCE} ..." + for bin_name in ${BIN_NAMES}; do + copy_binary "${bin_name}" "${BIN_SOURCE}" "${BIN_INSTALL}" + done + return + fi + + # If the source looks like a URL, use HTTP to fetch. + if urlmatch "${BIN_SOURCE}"; then + echo "Fetching binaries:${bin_names_str} remotely from: ${BIN_SOURCE} ..." + for bin_name in ${BIN_NAMES}; do + local DEST="${BIN_INSTALL}/${bin_name}" + curl -f -o "${DEST}" "${BIN_SOURCE}/${bin_name}" + chmod 700 "${DEST}" + done + return + fi + + echo 'ERROR: couldn'"'"'t fetch binaries.' + exit 1 +} + +############################################################################### +# Installs device manager: fetches binaries, configures suidhelper, calls the +# install command on deviced. +# Globals: +# V23_DEVICE_DIR +# Arguments: +# source of binaries (optional) +# args for install command and for device manager (optional) +# Returns: +# None +############################################################################### +install() { + if [[ -e "${V23_DEVICE_DIR}" ]]; then + echo "${V23_DEVICE_DIR} already exists!" + exit 1 + fi + mkdir -p -m 711 "${V23_DEVICE_DIR}" + local -r BIN_INSTALL="${V23_DEVICE_DIR}/bin" + mkdir -m 700 "${BIN_INSTALL}" + + if [[ $# = 0 || "$1" == --* ]]; then + local -r BIN_SOURCE="" + else + local -r BIN_SOURCE="$1" + shift + fi + + local SINGLE_USER=false + local INIT_MODE=false + local DEVMGR_USER=$(whoami) + for ARG in $*; do + if [[ ${ARG} = "--" ]]; then + break + elif [[ ${ARG} = "--single_user" || ${ARG} = "--single_user=true" ]]; then + SINGLE_USER=true + elif [[ ${ARG} = "--init_mode" || ${ARG} = "--init_mode=true" ]]; then + INIT_MODE=true + elif [[ ${ARG%=*} = "--devuser" ]]; then + DEVMGR_USER="${ARG##*=}" + fi + done + + BIN_NAMES="deviced suidhelper restarter v23agentd" + if [[ ${INIT_MODE} == true ]]; then + BIN_NAMES="${BIN_NAMES} inithelper" + fi + + # Fetch the binaries. + get_binaries "${BIN_INSTALL}" "${BIN_SOURCE}" + for bin_name in ${BIN_NAMES}; do + local BINARY="${BIN_INSTALL}/${bin_name}" + if [[ ! -s "${BINARY}" ]]; then + echo "${BINARY} is empty." + exit 1 + fi + done + echo "Binaries are in ${BIN_INSTALL}." + + # Set up the suidhelper. + echo "Configuring helpers ..." + + if [[ ${SINGLE_USER} == false && ${DEVMGR_USER} == $(whoami) ]]; then + echo "Running in multi-user mode requires a --devuser=<user>" + echo "argument. This limits the following unfortunate chain of events:" + echo "install the device manager as yourself, associate an external blessee" + echo "with your local user name and the external blessee can invoke an app" + echo "which, because it has the same system name as the device manager," + echo "can use suidhelper to give itself root priviledge." + exit 1 + fi + if [[ ${SINGLE_USER}} == true && ${DEVMGR_USER} != $(whoami) ]]; then + echo "The --devuser flag is unnecessary in single-user mode because" + echo "all processes run as $(whoami)." + exit 1 + fi + local -r SETUID_SCRIPT="${BIN_INSTALL}/suidhelper" + if [[ ${SINGLE_USER} == false ]]; then + portable_chown -R "${DEVMGR_USER}:bin" "${V23_DEVICE_DIR}" + make_suid "${SETUID_SCRIPT}" + fi + local -r INIT_SCRIPT="${BIN_INSTALL}/inithelper" + if [[ ${INIT_MODE} == true ]]; then + make_suid "${INIT_SCRIPT}" + fi + echo "Helpers configured." + + # Install the device manager. + echo "Installing device manager under ${V23_DEVICE_DIR} ..." + echo "V23_DEVICE_DIR=${V23_DEVICE_DIR}" + run "${BIN_INSTALL}/deviced" install \ + --suid_helper="${SETUID_SCRIPT}" \ + --restarter="${BIN_INSTALL}/restarter" \ + --agent="${BIN_INSTALL}/v23agentd" \ + --init_helper="${INIT_SCRIPT}" "$@" + echo "Device manager installed." +} + +############################################################################### +# Determines the owner of the device manager +# Globals: +# V23_DEVICE_DIR +# Arguments: +# None +# Returns: +# user owning the device manager +############################################################################### +getdevowner() { + case "$(uname)" in + "Darwin") + ls -dl "${V23_DEVICE_DIR}" | awk '{print $3}' + ;; + "Linux") + echo $(stat -c "%U" "${V23_DEVICE_DIR}") + ;; + esac +} + +############################################################################### +# Uninstalls device manager: calls the uninstall command of deviced and removes +# the installation. +# Globals: +# V23_DEVICE_DIR +# Arguments: +# None +# Returns: +# None +############################################################################### +uninstall() { + if [[ ! -d "${V23_DEVICE_DIR}" ]]; then + echo "${V23_DEVICE_DIR} does not exist or is not a directory!" + exit 1 + fi + local -r BIN_INSTALL="${V23_DEVICE_DIR}/bin" + local -r SETUID_SCRIPT="${BIN_INSTALL}/suidhelper" + echo "Uninstalling device manager from ${V23_DEVICE_DIR} ..." + run "${BIN_INSTALL}/deviced" uninstall \ + --suid_helper="${SETUID_SCRIPT}" + + echo "Device manager uninstalled." + # Any data created underneath "${V23_DEVICE_DIR}" by the "deviced + # install" command would have been cleaned up already by "deviced uninstall". + # However, install() created "${V23_DEVICE_DIR}", so uninstall() needs + # to remove it (as well as data created by install(), like bin/*). + + run rm -rf "${V23_DEVICE_DIR}/bin" + rmdir "${V23_DEVICE_DIR}" + echo "Removed ${V23_DEVICE_DIR}" +} + +############################################################################### +# Starts device manager: calls the start command of deviced. +# Globals: +# V23_DEVICE_DIR +# Arguments: +# None +# Returns: +# None +############################################################################### +start() { + if [[ ! -d "${V23_DEVICE_DIR}" ]]; then + echo "${V23_DEVICE_DIR} does not exist or is not a directory!" + exit 1 + fi + local -r BIN_INSTALL="${V23_DEVICE_DIR}/bin" + run "${BIN_INSTALL}/deviced" start +} + +############################################################################### +# Stops device manager: calls the stop command of deviced. +# Globals: +# V23_DEVICE_DIR +# Arguments: +# None +# Returns: +# None +############################################################################### +stop() { + if [[ ! -d "${V23_DEVICE_DIR}" ]]; then + echo "${V23_DEVICE_DIR} does not exist or is not a directory!" + exit 1 + fi + local -r BIN_INSTALL="${V23_DEVICE_DIR}/bin" + run "${BIN_INSTALL}/deviced" stop +} + +main() { + if [[ -z "${V23_DEVICE_DIR}" ]]; then + echo 'No local device installation dir specified!' + usage + exit 1 + fi + if [[ -e "${V23_DEVICE_DIR}" && ! -d "${V23_DEVICE_DIR}" ]]; then + echo "${V23_DEVICE_DIR} is not a directory!" + usage + exit 1 + fi + + if [[ $# = 0 ]]; then + echo 'No command specified!' + usage + exit 1 + fi + local -r COMMAND="$1" + shift + case "${COMMAND}" in + install) + install "$@" + ;; + uninstall) + uninstall + ;; + start) + start + ;; + stop) + stop + ;; + *) + echo "Unrecognized command: ${COMMAND}!" + usage + exit 1 + esac +} + +main "$@" From d839bbc2f38e6e1ed13b93468304a9fedb6ff54e Mon Sep 17 00:00:00 2001 From: Razvan Musaloiu-E <razvanm@google.com> Date: Fri, 4 Jan 2019 14:55:50 -0800 Subject: [PATCH 4/5] Add back the applicationd/application and binaryd/binary Both of these are necessary to for starting an application using deviced/device. --- x/ref/services/application/application/doc.go | 160 +++++ .../services/application/application/impl.go | 321 ++++++++++ .../application/application/impl_test.go | 270 ++++++++ .../applicationd/applicationd_v23_test.go | 98 +++ .../application/applicationd/dispatcher.go | 67 ++ .../services/application/applicationd/doc.go | 78 +++ .../application/applicationd/impl_test.go | 553 ++++++++++++++++ .../services/application/applicationd/main.go | 64 ++ .../application/applicationd/only_for_test.go | 9 + .../application/applicationd/perms_test.go | 351 +++++++++++ .../application/applicationd/service.go | 454 +++++++++++++ .../application/applicationd/v23_test.go | 15 + x/ref/services/binary/binary/doc.go | 151 +++++ x/ref/services/binary/binary/impl.go | 173 +++++ x/ref/services/binary/binary/impl_test.go | 160 +++++ .../binary/binaryd/binaryd_v23_test.go | 220 +++++++ x/ref/services/binary/binaryd/doc.go | 80 +++ x/ref/services/binary/binaryd/main.go | 113 ++++ x/ref/services/binary/tidy/appd/mock.go | 55 ++ x/ref/services/binary/tidy/binaryd/mock.go | 98 +++ x/ref/services/binary/tidy/doc.go | 124 ++++ x/ref/services/binary/tidy/impl.go | 225 +++++++ x/ref/services/binary/tidy/impl_test.go | 384 +++++++++++ x/ref/services/internal/fs/only_for_test.go | 47 ++ x/ref/services/internal/fs/simplestore.go | 573 +++++++++++++++++ .../services/internal/fs/simplestore_test.go | 595 ++++++++++++++++++ 26 files changed, 5438 insertions(+) create mode 100644 x/ref/services/application/application/doc.go create mode 100644 x/ref/services/application/application/impl.go create mode 100644 x/ref/services/application/application/impl_test.go create mode 100644 x/ref/services/application/applicationd/applicationd_v23_test.go create mode 100644 x/ref/services/application/applicationd/dispatcher.go create mode 100644 x/ref/services/application/applicationd/doc.go create mode 100644 x/ref/services/application/applicationd/impl_test.go create mode 100644 x/ref/services/application/applicationd/main.go create mode 100644 x/ref/services/application/applicationd/only_for_test.go create mode 100644 x/ref/services/application/applicationd/perms_test.go create mode 100644 x/ref/services/application/applicationd/service.go create mode 100644 x/ref/services/application/applicationd/v23_test.go create mode 100644 x/ref/services/binary/binary/doc.go create mode 100644 x/ref/services/binary/binary/impl.go create mode 100644 x/ref/services/binary/binary/impl_test.go create mode 100644 x/ref/services/binary/binaryd/binaryd_v23_test.go create mode 100644 x/ref/services/binary/binaryd/doc.go create mode 100644 x/ref/services/binary/binaryd/main.go create mode 100644 x/ref/services/binary/tidy/appd/mock.go create mode 100644 x/ref/services/binary/tidy/binaryd/mock.go create mode 100644 x/ref/services/binary/tidy/doc.go create mode 100644 x/ref/services/binary/tidy/impl.go create mode 100644 x/ref/services/binary/tidy/impl_test.go create mode 100644 x/ref/services/internal/fs/only_for_test.go create mode 100644 x/ref/services/internal/fs/simplestore.go create mode 100644 x/ref/services/internal/fs/simplestore_test.go diff --git a/x/ref/services/application/application/doc.go b/x/ref/services/application/application/doc.go new file mode 100644 index 000000000..0aeda82c6 --- /dev/null +++ b/x/ref/services/application/application/doc.go @@ -0,0 +1,160 @@ +// Copyright 2018 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated via go generate. +// DO NOT UPDATE MANUALLY + +/* +Command application manages the Vanadium application repository. + +Usage: + application [flags] <command> + +The application commands are: + match Shows the first matching envelope that matches the given + profiles. + profiles Shows the profiles supported by the given application. + put Add the given envelope to the application for the given profiles. + remove removes the application envelope for the given profile. + edit edits the application envelope for the given profile. + help Display help for commands or topics + +The global flags are: + -alsologtostderr=true + log to standard error as well as files + -log_backtrace_at=:0 + when logging hits line file:N, emit a stack trace + -log_dir= + if non-empty, write log files to this directory + -logtostderr=false + log to standard error instead of files + -max_stack_buf_size=4292608 + max size in bytes of the buffer to use for logging stack traces + -metadata=<just specify -metadata to activate> + Displays metadata for the program and exits. + -stderrthreshold=2 + logs at or above this threshold go to stderr + -time=false + Dump timing information to stderr before exiting the program. + -v=0 + log level for V logs + -v23.credentials= + directory to use for storing security credentials + -v23.i18n-catalogue= + 18n catalogue files to load, comma separated + -v23.namespace.root=[/(dev.v.io:r:vprod:service:mounttabled)@ns.dev.v.io:8101] + local namespace root; can be repeated to provided multiple roots + -v23.permissions.file= + specify a perms file as <name>:<permsfile> + -v23.permissions.literal= + explicitly specify the runtime perms as a JSON-encoded access.Permissions. + Overrides all --v23.permissions.file flags + -v23.proxy= + object name of proxy service to use to export services across network + boundaries + -v23.tcp.address= + address to listen on + -v23.tcp.protocol= + protocol to listen with + -v23.vtrace.cache-size=1024 + The number of vtrace traces to store in memory + -v23.vtrace.collect-regexp= + Spans and annotations that match this regular expression will trigger trace + collection + -v23.vtrace.dump-on-shutdown=true + If true, dump all stored traces on runtime shutdown + -v23.vtrace.sample-rate=0 + Rate (from 0.0 to 1.0) to sample vtrace traces + -v23.vtrace.v=0 + The verbosity level of the log messages to be captured in traces + -vmodule= + comma-separated list of globpattern=N settings for filename-filtered logging + (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns baz or + *az or b* but not by bar/baz or baz.go or az or b.* + -vpath= + comma-separated list of regexppattern=N settings for file pathname-filtered + logging (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns + foo/bar/baz or fo.*az or oo/ba or b.z but not by foo/bar/baz.go or fo*az + +Application match + +Shows the first matching envelope that matches the given profiles. + +Usage: + application match [flags] <application> <profiles> + +<application> is the full name of the application. <profiles> is a non-empty +comma-separated list of profiles. + +Application profiles + +Returns a comma-separated list of profiles supported by the given application. + +Usage: + application profiles [flags] <application> + +<application> is the full name of the application. + +Application put + +Add the given envelope to the application for the given profiles. + +Usage: + application put [flags] <application> <profiles> [<envelope>] + +<application> is the full name of the application. <profiles> is a +comma-separated list of profiles. <envelope> is the file that contains a +JSON-encoded envelope. If this file is not provided, the user will be prompted +to enter the data manually. + +The application put flags are: + -overwrite=false + If true, put forces an overwrite of any existing envelope + +Application remove + +removes the application envelope for the given profile. + +Usage: + application remove [flags] <application> <profile> + +<application> is the full name of the application. <profile> is a profile. If +specified as '*', all profiles are removed. + +Application edit + +edits the application envelope for the given profile. + +Usage: + application edit [flags] <application> <profile> + +<application> is the full name of the application. <profile> is a profile. + +Application help - Display help for commands or topics + +Help with no args displays the usage of the parent command. + +Help with args displays the usage of the specified sub-command or help topic. + +"help ..." recursively displays help for all commands and topics. + +Usage: + application help [flags] [command/topic ...] + +[command/topic ...] optionally identifies a specific sub-command or help topic. + +The application help flags are: + -style=compact + The formatting style for help output: + compact - Good for compact cmdline output. + full - Good for cmdline output, shows all global flags. + godoc - Good for godoc processing. + shortonly - Only output short description. + Override the default by setting the CMDLINE_STYLE environment variable. + -width=<terminal width> + Format output to this target width in runes, or unlimited if width < 0. + Defaults to the terminal width if available. Override the default by setting + the CMDLINE_WIDTH environment variable. +*/ +package main diff --git a/x/ref/services/application/application/impl.go b/x/ref/services/application/application/impl.go new file mode 100644 index 000000000..c4a8b214a --- /dev/null +++ b/x/ref/services/application/application/impl.go @@ -0,0 +1,321 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The following enables go generate to generate the doc.go file. +//go:generate gendoc . + +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "strings" + "time" + + "v.io/v23/context" + "v.io/v23/services/application" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + _ "v.io/x/ref/runtime/factories/generic" + "v.io/x/ref/services/repository" +) + +func main() { + cmdline.HideGlobalFlagsExcept() + cmdline.Main(cmdRoot) +} + +func getEnvelopeJSON(ctx *context.T, app repository.ApplicationClientMethods, profiles []string) ([]byte, error) { + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + env, err := app.Match(ctx, profiles) + if err != nil { + return nil, err + } + j, err := json.MarshalIndent(env, "", " ") + if err != nil { + return nil, fmt.Errorf("MarshalIndent(%v) failed: %v", env, err) + } + return j, nil +} + +func putEnvelopeJSON(ctx *context.T, env *cmdline.Env, app repository.ApplicationClientMethods, profiles []string, j []byte, overwrite bool) error { + var envelope application.Envelope + if err := json.Unmarshal(j, &envelope); err != nil { + return fmt.Errorf("Unmarshal(%v) failed: %v", string(j), err) + } + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + results := make(chan struct { + profile string + err error + }, len(profiles)) + for _, profile := range profiles { + go func(profile string) { + results <- struct { + profile string + err error + }{profile, app.Put(ctx, profile, envelope, overwrite)} + }(profile) + } + resultsMap := make(map[string]error, len(profiles)) + for i := 0; i < len(profiles); i++ { + result := <-results + resultsMap[result.profile] = result.err + } + nErrors := 0 + for _, p := range profiles { + if err := resultsMap[p]; err == nil { + fmt.Fprintf(env.Stdout, "Application envelope added for profile %s.\n", p) + } else { + fmt.Fprintf(env.Stderr, "Failed adding application envelope for profile %s: %v.\n", p, err) + nErrors++ + } + } + if nErrors > 0 { + return fmt.Errorf("encountered %d errors", nErrors) + } + return nil +} + +func parseProfiles(s string) (ret []string) { + profiles := strings.Split(s, ",") + seen := make(map[string]bool) + for _, p := range profiles { + if p != "" && !seen[p] { + seen[p] = true + ret = append(ret, p) + } + } + return +} + +func promptUser(env *cmdline.Env, msg string) string { + fmt.Fprint(env.Stdout, msg) + var answer string + if _, err := fmt.Scanf("%s", &answer); err != nil { + return "" + } + return answer +} + +var cmdMatch = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runMatch), + Name: "match", + Short: "Shows the first matching envelope that matches the given profiles.", + Long: "Shows the first matching envelope that matches the given profiles.", + ArgsName: "<application> <profiles>", + ArgsLong: ` +<application> is the full name of the application. +<profiles> is a non-empty comma-separated list of profiles.`, +} + +func runMatch(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 2, len(args); expected != got { + return env.UsageErrorf("match: incorrect number of arguments, expected %d, got %d", expected, got) + } + name, profiles := args[0], parseProfiles(args[1]) + app := repository.ApplicationClient(name) + j, err := getEnvelopeJSON(ctx, app, profiles) + if err != nil { + return err + } + fmt.Fprintln(env.Stdout, string(j)) + return nil +} + +var cmdProfiles = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runProfiles), + Name: "profiles", + Short: "Shows the profiles supported by the given application.", + Long: "Returns a comma-separated list of profiles supported by the given application.", + ArgsName: "<application>", + ArgsLong: ` +<application> is the full name of the application.`, +} + +func runProfiles(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 1, len(args); expected != got { + return env.UsageErrorf("profiles: incorrect number of arguments, expected %d, got %d", expected, got) + } + name := args[0] + app := repository.ApplicationClient(name) + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + if profiles, err := app.Profiles(ctx); err != nil { + return err + } else { + fmt.Fprintln(env.Stdout, strings.Join(profiles, ",")) + return nil + } +} + +var cmdPut = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runPut), + Name: "put", + Short: "Add the given envelope to the application for the given profiles.", + Long: "Add the given envelope to the application for the given profiles.", + ArgsName: "<application> <profiles> [<envelope>]", + ArgsLong: ` +<application> is the full name of the application. +<profiles> is a comma-separated list of profiles. +<envelope> is the file that contains a JSON-encoded envelope. If this file is +not provided, the user will be prompted to enter the data manually.`, +} + +var overwriteFlag bool + +func init() { + cmdPut.Flags.BoolVar(&overwriteFlag, "overwrite", false, "If true, put forces an overwrite of any existing envelope") +} + +func runPut(ctx *context.T, env *cmdline.Env, args []string) error { + if got := len(args); got != 2 && got != 3 { + return env.UsageErrorf("put: incorrect number of arguments, expected 2 or 3, got %d", got) + } + name, profiles := args[0], parseProfiles(args[1]) + if len(profiles) == 0 { + return env.UsageErrorf("put: no profiles specified") + } + app := repository.ApplicationClient(name) + if len(args) == 3 { + envelope := args[2] + j, err := ioutil.ReadFile(envelope) + if err != nil { + return fmt.Errorf("ReadFile(%v): %v", envelope, err) + } + if err = putEnvelopeJSON(ctx, env, app, profiles, j, overwriteFlag); err != nil { + return err + } + return nil + } + envelope := application.Envelope{Args: []string{}, Env: []string{}, Packages: application.Packages{}} + j, err := json.MarshalIndent(envelope, "", " ") + if err != nil { + return fmt.Errorf("MarshalIndent() failed: %v", err) + } + if err := editAndPutEnvelopeJSON(ctx, env, app, profiles, j, overwriteFlag); err != nil { + return err + } + return nil +} + +var cmdRemove = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runRemove), + Name: "remove", + Short: "removes the application envelope for the given profile.", + Long: "removes the application envelope for the given profile.", + ArgsName: "<application> <profile>", + ArgsLong: ` +<application> is the full name of the application. +<profile> is a profile. If specified as '*', all profiles are removed.`, +} + +func runRemove(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 2, len(args); expected != got { + return env.UsageErrorf("remove: incorrect number of arguments, expected %d, got %d", expected, got) + } + name, profile := args[0], args[1] + app := repository.ApplicationClient(name) + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + if err := app.Remove(ctx, profile); err != nil { + return err + } + fmt.Fprintln(env.Stdout, "Application envelope removed successfully.") + return nil +} + +var cmdEdit = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runEdit), + Name: "edit", + Short: "edits the application envelope for the given profile.", + Long: "edits the application envelope for the given profile.", + ArgsName: "<application> <profile>", + ArgsLong: ` +<application> is the full name of the application. +<profile> is a profile.`, +} + +func runEdit(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 2, len(args); expected != got { + return env.UsageErrorf("edit: incorrect number of arguments, expected %d, got %d", expected, got) + } + name, profiles := args[0], parseProfiles(args[1]) + if numProfiles := len(profiles); numProfiles != 1 { + return env.UsageErrorf("edit: incorrect number of profiles, expected 1, got %d", numProfiles) + } + app := repository.ApplicationClient(name) + + envData, err := getEnvelopeJSON(ctx, app, profiles) + if err != nil { + return err + } + if err := editAndPutEnvelopeJSON(ctx, env, app, profiles, envData, true); err != nil { + return err + } + return nil +} + +func editAndPutEnvelopeJSON(ctx *context.T, env *cmdline.Env, app repository.ApplicationClientMethods, profiles []string, envData []byte, overwrite bool) error { + f, err := ioutil.TempFile("", "application-edit-") + if err != nil { + return fmt.Errorf("TempFile() failed: %v", err) + } + fileName := f.Name() + f.Close() + defer os.Remove(fileName) + if err = ioutil.WriteFile(fileName, envData, os.FileMode(0644)); err != nil { + return err + } + editor := env.Vars["EDITOR"] + if len(editor) == 0 { + editor = "nano" + } + for { + c := exec.Command("sh", "-c", fmt.Sprintf("%s %s", editor, fileName)) + c.Stdin = os.Stdin + c.Stdout = os.Stdout + c.Stderr = os.Stderr + if err := c.Run(); err != nil { + return fmt.Errorf("failed to run %s %s", editor, fileName) + } + newData, err := ioutil.ReadFile(fileName) + if err != nil { + fmt.Fprintf(env.Stdout, "Error: %v\n", err) + if ans := promptUser(env, "Try again? [y/N] "); strings.ToUpper(ans) == "Y" { + continue + } + return errors.New("aborted") + } + if bytes.Compare(envData, newData) == 0 { + fmt.Fprintln(env.Stdout, "Nothing changed") + return nil + } + if err = putEnvelopeJSON(ctx, env, app, profiles, newData, overwrite); err != nil { + fmt.Fprintf(env.Stdout, "Error: %v\n", err) + if ans := promptUser(env, "Try again? [y/N] "); strings.ToUpper(ans) == "Y" { + continue + } + return errors.New("aborted") + } + break + } + fmt.Fprintln(env.Stdout, "Application envelope updated successfully.") + return nil +} + +var cmdRoot = &cmdline.Command{ + Name: "application", + Short: "manages the Vanadium application repository", + Long: ` +Command application manages the Vanadium application repository. +`, + Children: []*cmdline.Command{cmdMatch, cmdProfiles, cmdPut, cmdRemove, cmdEdit}, +} diff --git a/x/ref/services/application/application/impl_test.go b/x/ref/services/application/application/impl_test.go new file mode 100644 index 000000000..66c350bd6 --- /dev/null +++ b/x/ref/services/application/application/impl_test.go @@ -0,0 +1,270 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "reflect" + "sort" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + _ "v.io/x/ref/runtime/factories/generic" + "v.io/x/ref/services/repository" + "v.io/x/ref/test" +) + +var ( + envelope = application.Envelope{ + Title: "fifa world cup", + Args: []string{"arg1", "arg2", "arg3"}, + Binary: application.SignedFile{File: "/path/to/binary"}, + Env: []string{"env1", "env2", "env3"}, + Packages: map[string]application.SignedFile{ + "pkg1": application.SignedFile{ + File: "/path/to/package1", + }, + }, + Restarts: 0, + RestartTimeWindow: 0, + } + jsonEnv = `{ + "Title": "fifa world cup", + "Args": [ + "arg1", + "arg2", + "arg3" + ], + "Binary": { + "File": "/path/to/binary", + "Signature": { + "Purpose": null, + "Hash": "", + "R": null, + "S": null + } + }, + "Publisher": "", + "Env": [ + "env1", + "env2", + "env3" + ], + "Packages": { + "pkg1": { + "File": "/path/to/package1", + "Signature": { + "Purpose": null, + "Hash": "", + "R": null, + "S": null + } + } + }, + "Restarts": 0, + "RestartTimeWindow": 0 +}` + profiles = "a,b,c,d" + serverOut = make(chan string, 10) +) + +// drainServerOut collects all the output from the serverOut channel into a +// slice sorted alphabetically. +func drainServerOut() []string { + ret := make([]string, 0) + for { + select { + case line := <-serverOut: + ret = append(ret, line) + default: + sort.Strings(ret) + return ret + } + } +} + +type server struct { + suffix string +} + +func (s *server) Match(ctx *context.T, _ rpc.ServerCall, profiles []string) (application.Envelope, error) { + ctx.VI(2).Infof("%v.Match(%v) was called", s.suffix, profiles) + return envelope, nil +} + +func (s *server) Put(ctx *context.T, _ rpc.ServerCall, profile string, env application.Envelope, overwrite bool) error { + ctx.VI(2).Infof("%v.Put(%v, %v, %t) was called", s.suffix, profile, env, overwrite) + serverOut <- fmt.Sprintf("Put(%s, ..., %t)", profile, overwrite) + return nil +} + +func (s *server) Profiles(ctx *context.T, _ rpc.ServerCall) ([]string, error) { + ctx.VI(2).Infof("%v.Profiles() was called", s.suffix) + return strings.Split(profiles, ","), nil +} + +func (s *server) Remove(ctx *context.T, _ rpc.ServerCall, profile string) error { + ctx.VI(2).Infof("%v.Remove(%v) was called", s.suffix, profile) + return nil +} + +func (s *server) SetPermissions(ctx *context.T, _ rpc.ServerCall, perms access.Permissions, version string) error { + ctx.VI(2).Infof("%v.SetPermissions(%v, %v) was called", perms, version) + return nil +} + +func (s *server) GetPermissions(ctx *context.T, _ rpc.ServerCall) (access.Permissions, string, error) { + ctx.VI(2).Infof("%v.GetPermissions() was called") + return nil, "", nil +} + +func (s *server) TidyNow(ctx *context.T, _ rpc.ServerCall) error { + ctx.VI(2).Infof("%v.TidyNow() was called", s) + return nil +} + +type dispatcher struct{} + +func (d *dispatcher) Lookup(_ *context.T, suffix string) (interface{}, security.Authorizer, error) { + return repository.ApplicationServer(&server{suffix: suffix}), nil, nil +} + +func TestApplicationClient(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + ctx, server, err := v23.WithNewDispatchingServer(ctx, "", &dispatcher{}) + if err != nil { + t.Errorf("NewServer failed: %v", err) + return + } + endpoint := server.Status().Endpoints[0] + + // Setup the command-line. + var stdout, stderr bytes.Buffer + resetOut := func() { + stdout.Reset() + stderr.Reset() + } + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + appName := naming.JoinAddressName(endpoint.String(), "myapp/1") + oneProfile := "myprofile" + severalProfiles := "myprofile1,myprofile2,myprofile1" + + // Test the 'Match' command. + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"match", appName, oneProfile}); err != nil { + t.Fatalf("%v", err) + } + if expected, got := jsonEnv, strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected output from match. Got %q, expected %q", got, expected) + } + resetOut() + + // Test the 'put' command. + f, err := ioutil.TempFile("", "test") + if err != nil { + t.Fatalf("%v", err) + } + fileName := f.Name() + defer os.Remove(fileName) + if _, err = f.Write([]byte(jsonEnv)); err != nil { + t.Fatalf("%v", err) + } + if err = f.Close(); err != nil { + t.Fatalf("%v", err) + } + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"put", appName, severalProfiles, fileName}); err != nil { + t.Fatalf("%v", err) + } + if expected, got := "Application envelope added for profile myprofile1.\nApplication envelope added for profile myprofile2.", strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected output from put. Got %q, expected %q", got, expected) + } + if expected, got := []string{ + "Put(myprofile1, ..., false)", + "Put(myprofile2, ..., false)", + }, drainServerOut(); !reflect.DeepEqual(expected, got) { + t.Errorf("Unexpected output from mock server. Got %v, expected %v", got, expected) + } + resetOut() + + // Test the 'put' command with overwrite = true. + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"put", "--overwrite", appName, oneProfile, fileName}); err != nil { + t.Fatalf("%v", err) + } + if expected, got := "Application envelope added for profile myprofile.", strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected output from put. Got %q, expected %q", got, expected) + } + if expected, got := []string{"Put(myprofile, ..., true)"}, drainServerOut(); !reflect.DeepEqual(got, expected) { + t.Errorf("Unexpected output from mock server. Got %v, expected %v", got, expected) + } + resetOut() + + // Test the 'put' command with no profiles. + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"put", appName, ",,,", fileName}); err == nil { + t.Errorf("Expected put with no profiles to fail") + } else if expected, got := "ERROR: put: no profiles specified", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Errorf("Unexpected stderr output from put. Got %q, expected %q", got, expected+" ...") + } + resetOut() + + // Test the 'remove' command. + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"remove", appName, oneProfile}); err != nil { + t.Fatalf("%v", err) + } + if expected, got := "Application envelope removed successfully.", strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected output from remove. Got %q, expected %q", got, expected) + } + resetOut() + + // Test the 'edit' command. (nothing changed) + env.Vars = map[string]string{"EDITOR": "true"} + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"edit", appName, oneProfile}); err != nil { + t.Fatalf("%v", err) + } + if expected, got := "Nothing changed", strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected output from edit. Got %q, expected %q", got, expected) + } + resetOut() + + // Test the 'edit' command. + env.Vars = map[string]string{"EDITOR": "perl -pi -e 's/arg1/arg111/'"} + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"edit", appName, oneProfile}); err != nil { + t.Fatalf("%v", err) + } + if expected, got := "Application envelope added for profile myprofile.\nApplication envelope updated successfully.", strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected output from edit. Got %q, expected %q", got, expected) + } + resetOut() + + // Test the 'edit' command with more than 1 profiles. + env.Vars = map[string]string{"EDITOR": "true"} + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"edit", appName, severalProfiles}); err == nil { + t.Errorf("Expected edit with two profiles to fail") + } else if expected, got := "ERROR: edit: incorrect number of profiles, expected 1, got 2", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) { + t.Errorf("Unexpected stderr output from edit. Got %q, expected %q", got, expected+" ...") + } + resetOut() + + // Test the 'profiles' command. + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"profiles", appName}); err != nil { + t.Fatalf("%v", err) + } + if expected, got := profiles, strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected output from profiles. Got %q, expected %q", got, expected) + } + resetOut() +} diff --git a/x/ref/services/application/applicationd/applicationd_v23_test.go b/x/ref/services/application/applicationd/applicationd_v23_test.go new file mode 100644 index 000000000..587a2661b --- /dev/null +++ b/x/ref/services/application/applicationd/applicationd_v23_test.go @@ -0,0 +1,98 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "encoding/json" + "strings" + "testing" + + "v.io/v23/naming" + "v.io/v23/services/application" + "v.io/x/ref/test/v23test" +) + +func helper(t *testing.T, sh *v23test.Shell, clientBin string, clientCreds *v23test.Credentials, expectError bool, cmd string, args ...string) string { + args = append([]string{cmd}, args...) + c := sh.Cmd(clientBin, args...).WithCredentials(clientCreds) + c.ExitErrorIsOk = true + stdout := c.Stdout() + if c.Err != nil && !expectError { + t.Fatalf("%s %q failed: %v\n%v", clientBin, strings.Join(args, " "), c.Err, stdout) + } + if c.Err == nil && expectError { + t.Fatalf("%s %q did not fail when it should", clientBin, strings.Join(args, " ")) + } + return strings.TrimSpace(stdout) +} + +func matchEnvelope(t *testing.T, sh *v23test.Shell, clientBin string, clientCreds *v23test.Credentials, expectError bool, name, suffix string) string { + return helper(t, sh, clientBin, clientCreds, expectError, "match", naming.Join(name, suffix), "test-profile") +} + +func putEnvelope(t *testing.T, sh *v23test.Shell, clientBin string, clientCreds *v23test.Credentials, name, suffix, envelope string) string { + return helper(t, sh, clientBin, clientCreds, false, "put", naming.Join(name, suffix), "test-profile", envelope) +} + +func removeEnvelope(t *testing.T, sh *v23test.Shell, clientBin string, clientCreds *v23test.Credentials, name, suffix string) string { + return helper(t, sh, clientBin, clientCreds, false, "remove", naming.Join(name, suffix), "test-profile") +} + +func TestV23ApplicationRepository(t *testing.T) { + v23test.SkipUnlessRunningIntegrationTests(t) + sh := v23test.NewShell(t, nil) + defer sh.Cleanup() + sh.StartRootMountTable() + + // Start the application repository. + appRepoName := "test-app-repo" + sh.Cmd(v23test.BuildGoPkg(sh, "v.io/x/ref/services/application/applicationd"), + "-name="+appRepoName, + "-store="+sh.MakeTempDir(), + "-v=2", + "-v23.tcp.address=127.0.0.1:0").WithCredentials(sh.ForkCredentials("applicationd")).Start() + + // Build the client binary (must be a delegate of the server to pass + // the default authorization policy). + clientBin := v23test.BuildGoPkg(sh, "v.io/x/ref/services/application/application") + clientCreds := sh.ForkCredentials("applicationd:client") + + // Generate publisher blessings + publisher := sh.ForkCredentials("publisher") + sig, err := publisher.Principal.Sign([]byte("binarycontents")) + if err != nil { + t.Fatal(err) + } + // Create an application envelope. + appRepoSuffix := "test-application/v1" + appEnvelopeFile := sh.MakeTempFile() + publisherBlessings, _ := publisher.Principal.BlessingStore().Default() + wantEnvelope, err := json.MarshalIndent(application.Envelope{ + Title: "title", + Binary: application.SignedFile{ + File: "foo", + Signature: sig, + }, + Publisher: publisherBlessings, + }, "", " ") + if err != nil { + t.Fatal(err) + } + if _, err := appEnvelopeFile.Write([]byte(wantEnvelope)); err != nil { + t.Fatalf("Write() failed: %v", err) + } + putEnvelope(t, sh, clientBin, clientCreds, appRepoName, appRepoSuffix, appEnvelopeFile.Name()) + + // Match the application envelope. + if got, want := matchEnvelope(t, sh, clientBin, clientCreds, false, appRepoName, appRepoSuffix), string(wantEnvelope); got != want { + t.Fatalf("unexpected output: got %v, want %v", got, want) + } + + // Remove the application envelope. + removeEnvelope(t, sh, clientBin, clientCreds, appRepoName, appRepoSuffix) + + // Check that the application envelope no longer exists. + matchEnvelope(t, sh, clientBin, clientCreds, true, appRepoName, appRepoSuffix) +} diff --git a/x/ref/services/application/applicationd/dispatcher.go b/x/ref/services/application/applicationd/dispatcher.go new file mode 100644 index 000000000..4761814df --- /dev/null +++ b/x/ref/services/application/applicationd/dispatcher.go @@ -0,0 +1,67 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "path/filepath" + + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/verror" + "v.io/x/ref/services/internal/fs" + "v.io/x/ref/services/internal/pathperms" + "v.io/x/ref/services/repository" +) + +// dispatcher holds the state of the application repository dispatcher. +type dispatcher struct { + store *fs.Memstore + storeRoot string +} + +// NewDispatcher is the dispatcher factory. storeDir is a path to a directory in which to +// serialize the applicationd state. +func NewDispatcher(storeDir string) (rpc.Dispatcher, error) { + store, err := fs.NewMemstore(filepath.Join(storeDir, "applicationdstate.db")) + if err != nil { + return nil, err + } + return &dispatcher{store: store, storeRoot: storeDir}, nil +} + +func (d *dispatcher) Lookup(_ *context.T, suffix string) (interface{}, security.Authorizer, error) { + name, _, err := parse(nil, suffix) + if err != nil { + return nil, nil, err + } + + auth, err := pathperms.NewHierarchicalAuthorizer( + naming.Join("/acls", "data"), + naming.Join("/acls", name, "data"), + (*applicationPermsStore)(d.store)) + if err != nil { + return nil, nil, err + } + return repository.ApplicationServer(NewApplicationService(d.store, d.storeRoot, suffix)), auth, nil +} + +type applicationPermsStore fs.Memstore + +// PermsForPath implements PermsGetter so that applicationd can use the +// hierarchicalAuthorizer. +func (store *applicationPermsStore) PermsForPath(ctx *context.T, path string) (access.Permissions, bool, error) { + perms, _, err := getPermissions(ctx, (*fs.Memstore)(store), path) + + if verror.ErrorID(err) == verror.ErrNoExist.ID { + return nil, true, nil + } + if err != nil { + return nil, false, err + } + return perms, false, nil +} diff --git a/x/ref/services/application/applicationd/doc.go b/x/ref/services/application/applicationd/doc.go new file mode 100644 index 000000000..9da82aeb9 --- /dev/null +++ b/x/ref/services/application/applicationd/doc.go @@ -0,0 +1,78 @@ +// Copyright 2018 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated via go generate. +// DO NOT UPDATE MANUALLY + +/* +Command applicationd runs the application daemon, which implements the +v.io/x/ref/services/repository.Application interface. + +Usage: + applicationd [flags] + +The applicationd flags are: + -name= + Name to mount the application repository as. + -store= + Local directory to store application envelopes in. + +The global flags are: + -alsologtostderr=true + log to standard error as well as files + -log_backtrace_at=:0 + when logging hits line file:N, emit a stack trace + -log_dir= + if non-empty, write log files to this directory + -logtostderr=false + log to standard error instead of files + -max_stack_buf_size=4292608 + max size in bytes of the buffer to use for logging stack traces + -metadata=<just specify -metadata to activate> + Displays metadata for the program and exits. + -stderrthreshold=2 + logs at or above this threshold go to stderr + -time=false + Dump timing information to stderr before exiting the program. + -v=0 + log level for V logs + -v23.credentials= + directory to use for storing security credentials + -v23.i18n-catalogue= + 18n catalogue files to load, comma separated + -v23.namespace.root=[/(dev.v.io:r:vprod:service:mounttabled)@ns.dev.v.io:8101] + local namespace root; can be repeated to provided multiple roots + -v23.permissions.file= + specify a perms file as <name>:<permsfile> + -v23.permissions.literal= + explicitly specify the runtime perms as a JSON-encoded access.Permissions. + Overrides all --v23.permissions.file flags + -v23.proxy= + object name of proxy service to use to export services across network + boundaries + -v23.tcp.address= + address to listen on + -v23.tcp.protocol= + protocol to listen with + -v23.vtrace.cache-size=1024 + The number of vtrace traces to store in memory + -v23.vtrace.collect-regexp= + Spans and annotations that match this regular expression will trigger trace + collection + -v23.vtrace.dump-on-shutdown=true + If true, dump all stored traces on runtime shutdown + -v23.vtrace.sample-rate=0 + Rate (from 0.0 to 1.0) to sample vtrace traces + -v23.vtrace.v=0 + The verbosity level of the log messages to be captured in traces + -vmodule= + comma-separated list of globpattern=N settings for filename-filtered logging + (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns baz or + *az or b* but not by bar/baz or baz.go or az or b.* + -vpath= + comma-separated list of regexppattern=N settings for file pathname-filtered + logging (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns + foo/bar/baz or fo.*az or oo/ba or b.z but not by foo/bar/baz.go or fo*az +*/ +package main diff --git a/x/ref/services/application/applicationd/impl_test.go b/x/ref/services/application/applicationd/impl_test.go new file mode 100644 index 000000000..08c75d417 --- /dev/null +++ b/x/ref/services/application/applicationd/impl_test.go @@ -0,0 +1,553 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "fmt" + "io/ioutil" + "os" + "reflect" + "testing" + + v23 "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/security" + "v.io/v23/services/application" + "v.io/v23/verror" + "v.io/x/ref/runtime/factories/library" + appd "v.io/x/ref/services/application/applicationd" + "v.io/x/ref/services/repository" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" +) + +func init() { + // Allow v23.Init to be called multiple times. + library.AllowMultipleInitializations = true +} + +func newPublisherSignature(t *testing.T, ctx *context.T, msg []byte) (security.Blessings, security.Signature) { + // Generate publisher blessings + p := v23.GetPrincipal(ctx) + b, err := p.BlessSelf("publisher") + if err != nil { + t.Fatal(err) + } + sig, err := p.Sign(msg) + if err != nil { + t.Fatal(err) + } + return b, sig +} + +func checkEnvelope(t *testing.T, ctx *context.T, expected application.Envelope, stub repository.ApplicationClientStub, profiles ...string) { + if output, err := stub.Match(ctx, profiles); err != nil { + t.Fatal(testutil.FormatLogLine(2, "Match() failed: %v", err)) + } else if !reflect.DeepEqual(expected, output) { + t.Fatal(testutil.FormatLogLine(2, "Incorrect Match output: expected %#v, got %#v", expected, output)) + } +} + +func checkNoEnvelope(t *testing.T, ctx *context.T, stub repository.ApplicationClientStub, profiles ...string) { + if _, err := stub.Match(ctx, profiles); err == nil || verror.ErrorID(err) != verror.ErrNoExist.ID { + t.Fatal(testutil.FormatLogLine(2, "Unexpected error: expected %v, got %v", verror.ErrNoExist, err)) + } +} + +func checkProfiles(t *testing.T, ctx *context.T, stub repository.ApplicationClientStub, expected ...string) { + if output, err := stub.Profiles(ctx); err != nil { + t.Fatal(testutil.FormatLogLine(2, "Profiles() failed: %v", err)) + } else if !reflect.DeepEqual(expected, output) { + t.Fatal(testutil.FormatLogLine(2, "Incorrect Profiles output: expected %v, got %v", expected, output)) + } +} + +func checkNoProfile(t *testing.T, ctx *context.T, stub repository.ApplicationClientStub) { + if _, err := stub.Profiles(ctx); err == nil || verror.ErrorID(err) != verror.ErrNoExist.ID { + t.Fatal(testutil.FormatLogLine(2, "Unexpected error: expected %v, got %v", verror.ErrNoExist, err)) + } +} + +// TestInterface tests that the implementation correctly implements +// the Application interface. +func TestInterface(t *testing.T) { + ctx, shutdown := test.V23InitWithMounttable() + defer shutdown() + + dir, prefix := "", "" + store, err := ioutil.TempDir(dir, prefix) + if err != nil { + t.Fatalf("TempDir(%q, %q) failed: %v", dir, prefix, err) + } + defer os.RemoveAll(store) + dispatcher, err := appd.NewDispatcher(store) + if err != nil { + t.Fatalf("NewDispatcher() failed: %v", err) + } + + ctx, cancel := context.WithCancel(ctx) + _, server, err := v23.WithNewDispatchingServer(ctx, "", dispatcher) + if err != nil { + t.Fatalf("NewServer(%v) failed: %v", dispatcher, err) + } + endpoint := server.Status().Endpoints[0].String() + + // Create client stubs for talking to the server. + stub := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search")) + stubV0 := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search/v0")) + stubV1 := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search/v1")) + stubV2 := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search/v2")) + stubV3 := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search/v3")) + + blessings, sig := newPublisherSignature(t, ctx, []byte("binarycontents")) + + // Create example envelopes. + envelopeV1 := application.Envelope{ + Args: []string{"--help"}, + Env: []string{"DEBUG=1"}, + Binary: application.SignedFile{ + File: "/v23/name/of/binary", + Signature: sig, + }, + Publisher: blessings, + } + envelopeV2 := application.Envelope{ + Args: []string{"--verbose"}, + Env: []string{"DEBUG=0"}, + Binary: application.SignedFile{ + File: "/v23/name/of/binary", + Signature: sig, + }, + Publisher: blessings, + } + envelopeV3 := application.Envelope{ + Args: []string{"--verbose", "--spiffynewflag"}, + Env: []string{"DEBUG=0"}, + Binary: application.SignedFile{ + File: "/v23/name/of/binary", + Signature: sig, + }, + Publisher: blessings, + } + checkNoProfile(t, ctx, stub) + + // Test Put(), adding a number of application envelopes. + if err := stubV1.Put(ctx, "base", envelopeV1, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + if err := stubV1.Put(ctx, "media", envelopeV1, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + if err := stubV2.Put(ctx, "base", envelopeV2, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + if err := stub.Put(ctx, "base", envelopeV1, false); err == nil || verror.ErrorID(err) != appd.ErrInvalidSuffix.ID { + t.Fatalf("Unexpected error: expected %v, got %v", appd.ErrInvalidSuffix, err) + } + + // Test Match() against versioned names, trying profiles that do and + // don't have any envelopes uploaded. + checkNoEnvelope(t, ctx, stubV2) + checkEnvelope(t, ctx, envelopeV2, stubV2, "base") + checkNoEnvelope(t, ctx, stubV2, "media") + checkEnvelope(t, ctx, envelopeV2, stubV2, "base", "media") + checkEnvelope(t, ctx, envelopeV2, stubV2, "media", "base") + + // Test that Match() against a name without a version suffix returns the + // latest. + checkEnvelope(t, ctx, envelopeV2, stub, "base", "media") + checkEnvelope(t, ctx, envelopeV1, stub, "media") + + checkProfiles(t, ctx, stub, "base", "media") + checkProfiles(t, ctx, stubV1, "base", "media") + checkProfiles(t, ctx, stubV2, "base") + checkNoProfile(t, ctx, stubV3) + + // Test that if we add another envelope for a version that's the highest + // in sort order, the new envelope becomes the latest. + if err := stubV3.Put(ctx, "base", envelopeV3, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + checkEnvelope(t, ctx, envelopeV3, stub, "base", "media") + checkProfiles(t, ctx, stubV3, "base") + + // Test that this is not based on time but on sort order. + envelopeV0 := application.Envelope{ + Args: []string{"--help", "--zeroth"}, + Env: []string{"DEBUG=1"}, + Binary: application.SignedFile{ + File: "/v23/name/of/binary", + Signature: sig, + }, + Publisher: blessings, + } + if err := stubV0.Put(ctx, "base", envelopeV0, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + checkEnvelope(t, ctx, envelopeV3, stub, "base", "media") + + // Test Glob + matches, _, err := testutil.GlobName(ctx, naming.JoinAddressName(endpoint, ""), "...") + if err != nil { + t.Errorf("Unexpected Glob error: %v", err) + } + expected := []string{ + "", + "search", + "search/v0", + "search/v1", + "search/v2", + "search/v3", + } + if !reflect.DeepEqual(matches, expected) { + t.Errorf("unexpected Glob results. Got %q, want %q", matches, expected) + } + + // Put cannot replace the envelope for v0-base when overwrite is false. + if err := stubV0.Put(ctx, "base", envelopeV2, false); err == nil || verror.ErrorID(err) != verror.ErrExist.ID { + t.Fatalf("Unexpected error: expected %v, got %v", appd.ErrInvalidSuffix, err) + } + checkEnvelope(t, ctx, envelopeV0, stubV0, "base") + // Put can replace the envelope for v0-base when overwrite is true. + if err := stubV0.Put(ctx, "base", envelopeV2, true); err != nil { + t.Fatalf("Put() failed: %v", err) + } + checkEnvelope(t, ctx, envelopeV2, stubV0, "base") + + // Test Remove(), trying to remove both existing and non-existing + // application envelopes. + if err := stubV1.Remove(ctx, "base"); err != nil { + t.Fatalf("Remove() failed: %v", err) + } + checkNoEnvelope(t, ctx, stubV1) + checkEnvelope(t, ctx, envelopeV1, stubV1, "media") + checkNoEnvelope(t, ctx, stubV1, "base") + checkEnvelope(t, ctx, envelopeV1, stubV1, "base", "media") + checkEnvelope(t, ctx, envelopeV1, stubV1, "media", "base") + + if err := stubV1.Remove(ctx, "base"); err == nil || verror.ErrorID(err) != verror.ErrNoExist.ID { + t.Fatalf("Unexpected error: expected %v, got %v", verror.ErrNoExist, err) + } + if err := stub.Remove(ctx, "base"); err != nil { + t.Fatalf("Remove() failed: %v", err) + } + checkNoProfile(t, ctx, stubV0) + checkProfiles(t, ctx, stubV1, "media") + checkNoProfile(t, ctx, stubV2) + checkNoProfile(t, ctx, stubV3) + checkProfiles(t, ctx, stub, "media") + if err := stubV2.Remove(ctx, "media"); err == nil || verror.ErrorID(err) != verror.ErrNoExist.ID { + t.Fatalf("Unexpected error: expected %v, got %v", verror.ErrNoExist, err) + } + if err := stubV1.Remove(ctx, "media"); err != nil { + t.Fatalf("Remove() failed: %v", err) + } + checkNoProfile(t, ctx, stub) + + // Finally, use Match() to test that Remove really removed the + // application envelopes. + checkNoEnvelope(t, ctx, stubV1, "base") + checkNoEnvelope(t, ctx, stubV1, "media") + checkNoEnvelope(t, ctx, stubV2, "base") + + if err := stubV0.Put(ctx, "base", envelopeV0, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + if err := stubV1.Put(ctx, "base", envelopeV1, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + if err := stubV1.Put(ctx, "media", envelopeV1, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + if err := stubV2.Put(ctx, "base", envelopeV2, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + if err := stubV3.Put(ctx, "base", envelopeV3, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + if err := stub.Remove(ctx, "*"); err != nil { + t.Fatalf("Remove() failed: %v", err) + } + if err := stub.Remove(ctx, "*"); err == nil || verror.ErrorID(err) != verror.ErrNoExist.ID { + t.Fatalf("Unexpected error: expected %v, got %v", verror.ErrNoExist, err) + } + checkNoProfile(t, ctx, stub) + + // Shutdown the application repository server. + cancel() + <-server.Closed() +} + +func TestPreserveAcrossRestarts(t *testing.T) { + ctx, shutdown := test.V23InitWithMounttable() + defer shutdown() + + dir, prefix := "", "" + storedir, err := ioutil.TempDir(dir, prefix) + if err != nil { + t.Fatalf("TempDir(%q, %q) failed: %v", dir, prefix, err) + } + defer os.RemoveAll(storedir) + + dispatcher, err := appd.NewDispatcher(storedir) + if err != nil { + t.Fatalf("NewDispatcher() failed: %v", err) + } + + sctx, cancel := context.WithCancel(ctx) + _, server, err := v23.WithNewDispatchingServer(sctx, "", dispatcher) + if err != nil { + t.Fatalf("Serve(%v) failed: %v", dispatcher, err) + } + endpoint := server.Status().Endpoints[0].String() + + // Create client stubs for talking to the server. + stubV1 := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search/v1")) + + blessings, sig := newPublisherSignature(t, ctx, []byte("binarycontents")) + + // Create example envelopes. + envelopeV1 := application.Envelope{ + Args: []string{"--help"}, + Env: []string{"DEBUG=1"}, + Binary: application.SignedFile{ + File: "/v23/name/of/binary", + Signature: sig, + }, + Publisher: blessings, + } + + if err := stubV1.Put(ctx, "media", envelopeV1, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + + // There is content here now. + checkEnvelope(t, ctx, envelopeV1, stubV1, "media") + + cancel() + <-server.Closed() + + // Setup and start a second application server. + dispatcher, err = appd.NewDispatcher(storedir) + if err != nil { + t.Fatalf("NewDispatcher() failed: %v", err) + } + + _, server, err = v23.WithNewDispatchingServer(ctx, "", dispatcher) + if err != nil { + t.Fatalf("NewServer(%v) failed: %v", dispatcher, err) + } + endpoint = server.Status().Endpoints[0].String() + + stubV1 = repository.ApplicationClient(naming.JoinAddressName(endpoint, "search/v1")) + + checkEnvelope(t, ctx, envelopeV1, stubV1, "media") +} + +// TestTidyNow tests that TidyNow operates correctly. +func TestTidyNow(t *testing.T) { + ctx, shutdown := test.V23InitWithMounttable() + defer shutdown() + + dir, prefix := "", "" + store, err := ioutil.TempDir(dir, prefix) + if err != nil { + t.Fatalf("TempDir(%q, %q) failed: %v", dir, prefix, err) + } + defer os.RemoveAll(store) + dispatcher, err := appd.NewDispatcher(store) + if err != nil { + t.Fatalf("NewDispatcher() failed: %v", err) + } + + ctx, cancel := context.WithCancel(ctx) + _, server, err := v23.WithNewDispatchingServer(ctx, "", dispatcher) + if err != nil { + t.Fatalf("NewServer(%v) failed: %v", dispatcher, err) + } + defer func() { + cancel() + <-server.Closed() + }() + endpoint := server.Status().Endpoints[0].String() + + // Create client stubs for talking to the server. + stub := repository.ApplicationClient(naming.JoinAddressName(endpoint, "search")) + stubs := make([]repository.ApplicationClientStub, 0) + for _, vn := range []string{"v0", "v1", "v2", "v3"} { + s := repository.ApplicationClient(naming.JoinAddressName(endpoint, fmt.Sprintf("search/%s", vn))) + stubs = append(stubs, s) + } + blessings, sig := newPublisherSignature(t, ctx, []byte("binarycontents")) + + // Create example envelopes. + envelopeV1 := application.Envelope{ + Args: []string{"--help"}, + Env: []string{"DEBUG=1"}, + Binary: application.SignedFile{ + File: "/v23/name/of/binary", + Signature: sig, + }, + Publisher: blessings, + } + envelopeV2 := application.Envelope{ + Args: []string{"--verbose"}, + Env: []string{"DEBUG=0"}, + Binary: application.SignedFile{ + File: "/v23/name/of/binary", + Signature: sig, + }, + Publisher: blessings, + } + envelopeV3 := application.Envelope{ + Args: []string{"--verbose", "--spiffynewflag"}, + Env: []string{"DEBUG=0"}, + Binary: application.SignedFile{ + File: "/v23/name/of/binary", + Signature: sig, + }, + Publisher: blessings, + } + + stuffEnvelopes(t, ctx, stubs, []profEnvTuple{ + { + &envelopeV1, + []string{"base", "media"}, + }, + }) + + // Verify that we have one + testGlob(t, ctx, endpoint, []string{ + "", + "search", + "search/v0", + }) + + // Tidy when already tidy does not alter. + if err := stubs[0].TidyNow(ctx); err != nil { + t.Errorf("TidyNow failed: %v", err) + } + testGlob(t, ctx, endpoint, []string{ + "", + "search", + "search/v0", + }) + + stuffEnvelopes(t, ctx, stubs, []profEnvTuple{ + { + &envelopeV1, + []string{"base", "media"}, + }, + { + &envelopeV2, + []string{"media"}, + }, + { + &envelopeV3, + []string{"base"}, + }, + }) + + // Now there are three envelopes which is one more than the + // numberOfVersionsToKeep set for the test. However + // we need both envelopes v0 and v2 to keep two versions for + // profile media and envelopes v0 and v3 to keep two versions + // for profile base so we continue to have three versions. + if err := stubs[0].TidyNow(ctx); err != nil { + t.Errorf("TidyNow failed: %v", err) + } + testGlob(t, ctx, endpoint, []string{ + "", + "search", + "search/v0", + "search/v1", + "search/v2", + }) + + // And the newest version for each profile differs because + // not every version supports all profiles. + checkEnvelope(t, ctx, envelopeV2, stub, "media") + checkEnvelope(t, ctx, envelopeV3, stub, "base") + + // Test that we can add an envelope for v3 with profile media and after calling + // TidyNow(), there will be all versions still in glob but v0 will only match profile + // base and not have an envelope for profile media. + if err := stubs[3].Put(ctx, "media", envelopeV3, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + + if err := stubs[0].TidyNow(ctx); err != nil { + t.Errorf("TidyNow failed: %v", err) + } + testGlob(t, ctx, endpoint, []string{ + "", + "search", + "search/v0", + "search/v1", + "search/v2", + "search/v3", + }) + + checkEnvelope(t, ctx, envelopeV1, stubs[0], "base") + checkNoEnvelope(t, ctx, stubs[0], "media") + + stuffEnvelopes(t, ctx, stubs, []profEnvTuple{ + { + &envelopeV1, + []string{"base", "media"}, + }, + { + &envelopeV2, + []string{"base", "media"}, + }, + { + &envelopeV3, + []string{"base", "media"}, + }, + { + &envelopeV3, + []string{"base", "media"}, + }, + }) + + // Now there are four versions for all profiles so tidying + // will remove the older versions. + if err := stubs[0].TidyNow(ctx); err != nil { + t.Errorf("TidyNow failed: %v", err) + } + + testGlob(t, ctx, endpoint, []string{ + "", + "search", + "search/v2", + "search/v3", + }) +} + +type profEnvTuple struct { + e *application.Envelope + p []string +} + +func testGlob(t *testing.T, ctx *context.T, endpoint string, expected []string) { + matches, _, err := testutil.GlobName(ctx, naming.JoinAddressName(endpoint, ""), "...") + if err != nil { + t.Errorf("Unexpected Glob error: %v", err) + } + if !reflect.DeepEqual(matches, expected) { + t.Errorf("unexpected Glob results. Got %q, want %q", matches, expected) + } +} + +func stuffEnvelopes(t *testing.T, ctx *context.T, stubs []repository.ApplicationClientStub, pets []profEnvTuple) { + for i, pet := range pets { + for _, profile := range pet.p { + if err := stubs[i].Put(ctx, profile, *pet.e, true); err != nil { + t.Fatalf("%d: Put(%v) failed: %v", i, pet, err) + } + } + } +} diff --git a/x/ref/services/application/applicationd/main.go b/x/ref/services/application/applicationd/main.go new file mode 100644 index 000000000..a0dc7ed1a --- /dev/null +++ b/x/ref/services/application/applicationd/main.go @@ -0,0 +1,64 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The following enables go generate to generate the doc.go file. +//go:generate gendoc . -help + +package main + +import ( + "fmt" + + "v.io/v23" + "v.io/v23/context" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/signals" + "v.io/x/ref/lib/v23cmd" + _ "v.io/x/ref/runtime/factories/roaming" +) + +var name, store string + +func main() { + cmdAppD.Flags.StringVar(&name, "name", "", "Name to mount the application repository as.") + cmdAppD.Flags.StringVar(&store, "store", "", "Local directory to store application envelopes in.") + + cmdline.HideGlobalFlagsExcept() + cmdline.Main(cmdAppD) +} + +var cmdAppD = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runAppD), + Name: "applicationd", + Short: "Runs the application daemon.", + Long: ` +Command applicationd runs the application daemon, which implements the +v.io/x/ref/services/repository.Application interface. +`, +} + +func runAppD(ctx *context.T, env *cmdline.Env, args []string) error { + if store == "" { + return env.UsageErrorf("Specify a directory for storing application envelopes using --store=<name>") + } + + dispatcher, err := NewDispatcher(store) + if err != nil { + return fmt.Errorf("NewDispatcher() failed: %v", err) + } + + ctx, server, err := v23.WithNewDispatchingServer(ctx, name, dispatcher) + if err != nil { + return fmt.Errorf("NewServer() failed: %v", err) + } + epName := server.Status().Endpoints[0].Name() + if name != "" { + ctx.Infof("Application repository serving at %q (%q)", name, epName) + } else { + ctx.Infof("Application repository serving at %q", epName) + } + // Wait until shutdown. + <-signals.ShutdownOnSignals(ctx) + return nil +} diff --git a/x/ref/services/application/applicationd/only_for_test.go b/x/ref/services/application/applicationd/only_for_test.go new file mode 100644 index 000000000..0b3724ede --- /dev/null +++ b/x/ref/services/application/applicationd/only_for_test.go @@ -0,0 +1,9 @@ +// Copyright 2015 The Vanadium 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 main + +func init() { + numberOfVersionsToKeep = 2 +} diff --git a/x/ref/services/application/applicationd/perms_test.go b/x/ref/services/application/applicationd/perms_test.go new file mode 100644 index 000000000..a8e271f31 --- /dev/null +++ b/x/ref/services/application/applicationd/perms_test.go @@ -0,0 +1,351 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "fmt" + "reflect" + "testing" + + "v.io/v23" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/verror" + "v.io/x/lib/gosh" + "v.io/x/ref/lib/signals" + appd "v.io/x/ref/services/application/applicationd" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/services/repository" + "v.io/x/ref/test" + "v.io/x/ref/test/testutil" +) + +var appRepository = gosh.RegisterFunc("appRepository", func(publishName, storedir string) { + ctx, shutdown := test.V23InitWithMounttable() + defer shutdown() + + defer fmt.Printf("%v terminating\n", publishName) + defer ctx.VI(1).Infof("%v terminating", publishName) + + dispatcher, err := appd.NewDispatcher(storedir) + if err != nil { + ctx.Fatalf("Failed to create repository dispatcher: %v", err) + } + ctx, server, err := v23.WithNewDispatchingServer(ctx, publishName, dispatcher) + if err != nil { + ctx.Fatalf("NewDispatchingServer(%v) failed: %v", publishName, err) + } + ctx.VI(1).Infof("applicationd name: %v", server.Status().Endpoints[0].Name()) + + fmt.Println("READY") + <-signals.ShutdownOnSignals(ctx) +}) + +func TestApplicationUpdatePermissions(t *testing.T) { + ctx, shutdown := test.V23InitWithMounttable() + defer shutdown() + + // V23InitWithMounttable sets the context up with a self-signed principal, + // whose blessing (test-blessing) will act as the root blessing for the test. + const rootBlessing = test.TestBlessing + idp := testutil.IDProviderFromPrincipal(v23.GetPrincipal(ctx)) + // Call ourselves test-blessing:self, distinct from test-blessing:other + // which we'll give to the 'other' context. + if err := idp.Bless(v23.GetPrincipal(ctx), "self"); err != nil { + t.Fatal(err) + } + + sh, deferFn := servicetest.CreateShell(t, ctx) + defer deferFn() + + // setup mock up directory to put state in + storedir, cleanup := servicetest.SetupRootDir(t, "application") + defer cleanup() + + cmd := sh.FuncCmd(appRepository, "repo", storedir) + cmd.Start() + cmd.S.Expect("READY") + + otherCtx, err := v23.WithPrincipal(ctx, testutil.NewPrincipal()) + if err != nil { + t.Fatal(err) + } + if err := idp.Bless(v23.GetPrincipal(otherCtx), "other"); err != nil { + t.Fatal(err) + } + + v1stub := repository.ApplicationClient("repo/search/v1") + repostub := repository.ApplicationClient("repo") + + // Create example envelopes. + envelopeV1 := application.Envelope{ + Args: []string{"--help"}, + Env: []string{"DEBUG=1"}, + Binary: application.SignedFile{File: "/v23/name/of/binary"}, + } + + // Envelope putting as other should fail. + if err := v1stub.Put(otherCtx, "base", envelopeV1, false); verror.ErrorID(err) != verror.ErrNoAccess.ID { + t.Fatalf("Put() returned errorid=%v wanted errorid=%v [%v]", verror.ErrorID(err), verror.ErrNoAccess.ID, err) + } + + // Envelope putting as global should succeed. + if err := v1stub.Put(ctx, "base", envelopeV1, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + + ctx.VI(2).Infof("Accessing the Permission Lists of the root returns a (simulated) list providing default authorization.") + perms, version, err := repostub.GetPermissions(ctx) + if err != nil { + t.Fatalf("GetPermissions should not have failed: %v", err) + } + if got, want := version, ""; got != want { + t.Fatalf("GetPermissions got %v, want %v", got, want) + } + expectedInBps := []security.BlessingPattern{rootBlessing + ":$", rootBlessing + ":self:$", rootBlessing + ":self:child"} + expected := access.Permissions{ + "Admin": access.AccessList{In: expectedInBps, NotIn: []string(nil)}, + "Read": access.AccessList{In: expectedInBps, NotIn: []string(nil)}, + "Write": access.AccessList{In: expectedInBps, NotIn: []string(nil)}, + "Debug": access.AccessList{In: expectedInBps, NotIn: []string(nil)}, + "Resolve": access.AccessList{In: expectedInBps, NotIn: []string(nil)}, + } + if got := perms; !reflect.DeepEqual(expected.Normalize(), got.Normalize()) { + t.Errorf("got %#v, expected %#v ", got, expected) + } + + ctx.VI(2).Infof("self attempting to give other permission to update application") + newPerms := make(access.Permissions) + for _, tag := range access.AllTypicalTags() { + newPerms.Add(rootBlessing+":self", string(tag)) + newPerms.Add(rootBlessing+":other", string(tag)) + } + if err := repostub.SetPermissions(ctx, newPerms, ""); err != nil { + t.Fatalf("SetPermissions failed: %v", err) + } + + perms, version, err = repostub.GetPermissions(ctx) + if err != nil { + t.Fatalf("GetPermissions should not have failed: %v", err) + } + expected = newPerms + if got := perms; !reflect.DeepEqual(expected.Normalize(), got.Normalize()) { + t.Errorf("got %#v, exected %#v ", got, expected) + } + + // Envelope putting as other should now succeed. + if err := v1stub.Put(otherCtx, "base", envelopeV1, true); err != nil { + t.Fatalf("Put() wrongly failed: %v", err) + } + + // Other takes control. + perms, version, err = repostub.GetPermissions(otherCtx) + if err != nil { + t.Fatalf("GetPermissions 2 should not have failed: %v", err) + } + perms["Admin"] = access.AccessList{ + In: []security.BlessingPattern{rootBlessing + ":other"}, + NotIn: []string{}} + if err = repostub.SetPermissions(otherCtx, perms, version); err != nil { + t.Fatalf("SetPermissions failed: %v", err) + } + + // Self is now locked out but other isn't. + if _, _, err = repostub.GetPermissions(ctx); err == nil { + t.Fatalf("GetPermissions should not have succeeded") + } + perms, _, err = repostub.GetPermissions(otherCtx) + if err != nil { + t.Fatalf("GetPermissions should not have failed: %v", err) + } + expected = access.Permissions{ + "Admin": access.AccessList{In: []security.BlessingPattern{rootBlessing + ":other"}, NotIn: []string{}}, + "Read": access.AccessList{In: []security.BlessingPattern{rootBlessing + ":other", rootBlessing + ":self"}, NotIn: []string{}}, + "Write": access.AccessList{In: []security.BlessingPattern{rootBlessing + ":other", rootBlessing + ":self"}, NotIn: []string{}}, + "Debug": access.AccessList{In: []security.BlessingPattern{rootBlessing + ":other", rootBlessing + ":self"}, NotIn: []string{}}, + "Resolve": access.AccessList{In: []security.BlessingPattern{rootBlessing + ":other", rootBlessing + ":self"}, NotIn: []string{}}} + + if got := perms; !reflect.DeepEqual(expected.Normalize(), got.Normalize()) { + t.Errorf("got %#v, exected %#v ", got, expected) + } +} + +func TestPerAppPermissions(t *testing.T) { + ctx, shutdown := test.V23InitWithMounttable() + defer shutdown() + // By default, all principals in this test will have blessings generated based + // on the username/machine running this process. Give them recognizable names + // ("root:self" etc.), so the Permissions can be set deterministically. + idp := testutil.NewIDProvider("root") + if err := idp.Bless(v23.GetPrincipal(ctx), "self"); err != nil { + t.Fatal(err) + } + + sh, deferFn := servicetest.CreateShellAndMountTable(t, ctx) + defer deferFn() + + // setup mock up directory to put state in + storedir, cleanup := servicetest.SetupRootDir(t, "application") + defer cleanup() + + otherCtx, err := v23.WithPrincipal(ctx, testutil.NewPrincipal()) + if err != nil { + t.Fatal(err) + } + if err := idp.Bless(v23.GetPrincipal(otherCtx), "other"); err != nil { + t.Fatal(err) + } + + cmd := sh.FuncCmd(appRepository, "repo", storedir) + cmd.Start() + cmd.S.Expect("READY") + + // Create example envelope. + envelopeV1 := application.Envelope{ + Args: []string{"--help"}, + Env: []string{"DEBUG=1"}, + Binary: application.SignedFile{File: "/v23/name/of/binary"}, + } + + ctx.VI(2).Info("Upload an envelope") + v1stub := repository.ApplicationClient("repo/search/v1") + if err := v1stub.Put(ctx, "base", envelopeV1, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + v2stub := repository.ApplicationClient("repo/search/v2") + if err := v2stub.Put(ctx, "base", envelopeV1, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + v3stub := repository.ApplicationClient("repo/naps/v1") + if err := v3stub.Put(ctx, "base", envelopeV1, false); err != nil { + t.Fatalf("Put() failed: %v", err) + } + + ctx.VI(2).Info("Self can access Permissions but other can't.") + expectedSelfInBps := []security.BlessingPattern{"root:$", "root:self"} + expectedSelfPermissions := access.Permissions{ + "Admin": access.AccessList{In: expectedSelfInBps, NotIn: []string{}}, + "Read": access.AccessList{In: expectedSelfInBps, NotIn: []string{}}, + "Write": access.AccessList{In: expectedSelfInBps, NotIn: []string{}}, + "Debug": access.AccessList{In: expectedSelfInBps, NotIn: []string{}}, + "Resolve": access.AccessList{In: expectedSelfInBps, NotIn: []string{}}, + } + + for _, path := range []string{"repo/search", "repo/search/v1", "repo/search/v2", "repo/naps", "repo/naps/v1"} { + stub := repository.ApplicationClient(path) + perms, _, err := stub.GetPermissions(ctx) + if err != nil { + t.Fatalf("Newly uploaded envelopes failed to receive permission lists: %v", err) + } + + if got := perms; !reflect.DeepEqual(expectedSelfPermissions.Normalize(), got.Normalize()) { + t.Errorf("got %#v, expected %#v ", got, expectedSelfPermissions) + } + + // But otherCtx doesn't have admin permissions so has no access. + if _, _, err := stub.GetPermissions(otherCtx); err == nil { + t.Fatalf("GetPermissions didn't fail for other when it should have.") + } + } + + ctx.VI(2).Infof("Self sets root Permissions.") + repostub := repository.ApplicationClient("repo") + newPerms := make(access.Permissions) + for _, tag := range access.AllTypicalTags() { + newPerms.Add("root:self", string(tag)) + } + if err := repostub.SetPermissions(ctx, newPerms, ""); err != nil { + t.Fatalf("SetPermissions failed: %v", err) + } + + ctx.VI(2).Infof("Other still can't access anything.") + if _, _, err = repostub.GetPermissions(otherCtx); err == nil { + t.Fatalf("GetPermissions should have failed") + } + + ctx.VI(2).Infof("Self gives other full access to repo/search/...") + newPerms, version, err := v1stub.GetPermissions(ctx) + if err != nil { + t.Fatalf("GetPermissions should not have failed: %v", err) + } + for _, tag := range access.AllTypicalTags() { + newPerms.Add("root:other", string(tag)) + } + if err := v1stub.SetPermissions(ctx, newPerms, version); err != nil { + t.Fatalf("SetPermissions failed: %v", err) + } + + expectedInBps := []security.BlessingPattern{"root:$", "root:other", "root:self"} + expected := access.Permissions{ + "Resolve": access.AccessList{In: expectedInBps, NotIn: []string(nil)}, + "Admin": access.AccessList{In: expectedInBps, NotIn: []string(nil)}, + "Read": access.AccessList{In: expectedInBps, NotIn: []string(nil)}, + "Write": access.AccessList{In: expectedInBps, NotIn: []string(nil)}, + "Debug": access.AccessList{In: expectedInBps, NotIn: []string(nil)}, + } + + for _, path := range []string{"repo/search", "repo/search/v1", "repo/search/v2"} { + stub := repository.ApplicationClient(path) + ctx.VI(2).Infof("Other can now access this app independent of version.") + perms, _, err := stub.GetPermissions(otherCtx) + if err != nil { + t.Fatalf("GetPermissions should not have failed: %v", err) + } + + if got := perms; !reflect.DeepEqual(expected.Normalize(), got.Normalize()) { + t.Errorf("got %#v, expected %#v ", got, expected) + } + ctx.VI(2).Infof("Self can also access thanks to hierarchical auth.") + if _, _, err = stub.GetPermissions(ctx); err != nil { + t.Fatalf("GetPermissions should not have failed: %v", err) + } + } + + ctx.VI(2).Infof("But other locations are unaffected and other cannot access.") + for _, path := range []string{"repo/naps", "repo/naps/v1"} { + stub := repository.ApplicationClient(path) + if _, _, err := stub.GetPermissions(otherCtx); err == nil { + t.Fatalf("GetPermissions didn't fail when it should have.") + } + } + + // Self gives other write perms on base. + newPerms, version, err = repostub.GetPermissions(ctx) + if err != nil { + t.Fatalf("GetPermissions should not have failed: %v", err) + } + newPerms["Write"] = access.AccessList{In: []security.BlessingPattern{"root:other", "root:self"}} + if err := repostub.SetPermissions(ctx, newPerms, version); err != nil { + t.Fatalf("SetPermissions failed: %v", err) + } + + // Other can now upload an envelope at both locations. + for _, stub := range []repository.ApplicationClientStub{v1stub, v2stub} { + if err := stub.Put(otherCtx, "base", envelopeV1, true); err != nil { + t.Fatalf("Put() failed: %v", err) + } + } + + // But because application search already exists, the Permissions do not change. + for _, path := range []string{"repo/search", "repo/search/v1", "repo/search/v2"} { + stub := repository.ApplicationClient(path) + perms, _, err := stub.GetPermissions(otherCtx) + if err != nil { + t.Fatalf("GetPermissions should not have failed: %v", err) + } + if got := perms; !reflect.DeepEqual(expected.Normalize(), got.Normalize()) { + t.Errorf("got %#v, expected %#v ", got, expected) + } + } + + // But self didn't give other Permissions modification permissions. + for _, path := range []string{"repo/search", "repo/search/v2"} { + stub := repository.ApplicationClient(path) + if _, _, err := stub.GetPermissions(otherCtx); err != nil { + t.Fatalf("GetPermissions failed when it should not have for same application: %v", err) + } + } +} diff --git a/x/ref/services/application/applicationd/service.go b/x/ref/services/application/applicationd/service.go new file mode 100644 index 000000000..ea012ffab --- /dev/null +++ b/x/ref/services/application/applicationd/service.go @@ -0,0 +1,454 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "sort" + "strings" + + "v.io/v23/context" + "v.io/v23/glob" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/verror" + "v.io/x/lib/set" + "v.io/x/ref/services/internal/fs" + "v.io/x/ref/services/internal/pathperms" + "v.io/x/ref/services/repository" +) + +// appRepoService implements the Application repository interface. +type appRepoService struct { + // store is the storage server used for storing application + // metadata. + // All objects share the same Memstore. + store *fs.Memstore + // storeRoot is a name in the directory under which all data will be + // stored. + storeRoot string + // suffix is the name of the application object. + suffix string +} + +const pkgPath = "v.io/x/ref/services/application/applicationd" + +var ( + ErrInvalidSuffix = verror.Register(pkgPath+".InvalidSuffix", verror.NoRetry, "{1:}{2:} invalid suffix{:_}") + ErrOperationFailed = verror.Register(pkgPath+".OperationFailed", verror.NoRetry, "{1:}{2:} operation failed{:_}") + ErrNotAuthorized = verror.Register(pkgPath+".errNotAuthorized", verror.NoRetry, "{1:}{2:} none of the client's blessings are valid {:_}") +) + +// NewApplicationService returns a new Application service implementation. +func NewApplicationService(store *fs.Memstore, storeRoot, suffix string) repository.ApplicationServerMethods { + return &appRepoService{store: store, storeRoot: storeRoot, suffix: suffix} +} + +func parse(ctx *context.T, suffix string) (string, string, error) { + tokens := strings.Split(suffix, "/") + switch len(tokens) { + case 2: + return tokens[0], tokens[1], nil + case 1: + return tokens[0], "", nil + default: + return "", "", verror.New(ErrInvalidSuffix, ctx) + } +} + +func (i *appRepoService) Profiles(ctx *context.T, call rpc.ServerCall) ([]string, error) { + ctx.VI(0).Infof("%v.Profiles()", i.suffix) + name, version, err := parse(ctx, i.suffix) + if err != nil { + return []string{}, err + } + i.store.Lock() + defer i.store.Unlock() + + profiles, err := i.store.BindObject(naming.Join("/applications", name)).Children() + if err != nil { + return []string{}, err + } + if version == "" { + return profiles, nil + } + profilesRet := make(map[string]struct{}) + for _, profile := range profiles { + versions, err := i.store.BindObject(naming.Join("/applications", name, profile)).Children() + if err != nil { + return []string{}, err + } + for _, v := range versions { + if version == v { + profilesRet[profile] = struct{}{} + break + } + } + } + ret := set.String.ToSlice(profilesRet) + if len(ret) == 0 { + return []string{}, verror.New(verror.ErrNoExist, ctx) + } + sort.Strings(ret) + return ret, nil +} + +func (i *appRepoService) Match(ctx *context.T, call rpc.ServerCall, profiles []string) (application.Envelope, error) { + ctx.VI(0).Infof("%v.Match(%v)", i.suffix, profiles) + empty := application.Envelope{} + name, version, err := parse(ctx, i.suffix) + if err != nil { + return empty, err + } + + i.store.Lock() + defer i.store.Unlock() + + if version == "" { + versions, err := i.allAppVersionsForProfiles(name, profiles) + if err != nil { + return empty, err + } + if len(versions) < 1 { + return empty, verror.New(verror.ErrNoExist, ctx) + } + sort.Strings(versions) + version = versions[len(versions)-1] + } + + for _, profile := range profiles { + path := naming.Join("/applications", name, profile, version) + entry, err := i.store.BindObject(path).Get(call) + if err != nil { + continue + } + envelope, ok := entry.Value.(application.Envelope) + if !ok { + continue + } + return envelope, nil + } + return empty, verror.New(verror.ErrNoExist, ctx) +} + +func (i *appRepoService) Put(ctx *context.T, call rpc.ServerCall, profile string, envelope application.Envelope, overwrite bool) error { + ctx.VI(0).Infof("%v.Put(%v, %v, %t)", i.suffix, profile, envelope, overwrite) + name, version, err := parse(ctx, i.suffix) + if err != nil { + return err + } + if version == "" { + return verror.New(ErrInvalidSuffix, ctx) + } + i.store.Lock() + defer i.store.Unlock() + // Transaction is rooted at "", so tname == tid. + tname, err := i.store.BindTransactionRoot("").CreateTransaction(call) + if err != nil { + return err + } + + // Only add a Permissions value if there is not already one present. + apath := naming.Join("/acls", name, "data") + aobj := i.store.BindObject(apath) + if _, err := aobj.Get(call); verror.ErrorID(err) == fs.ErrNotInMemStore.ID { + rb, _ := security.RemoteBlessingNames(ctx, call.Security()) + if len(rb) == 0 { + // None of the client's blessings are valid. + return verror.New(ErrNotAuthorized, ctx) + } + newperms := pathperms.PermissionsForBlessings(rb) + if _, err := aobj.Put(nil, newperms); err != nil { + return err + } + } + + path := naming.Join(tname, "/applications", name, profile, version) + object := i.store.BindObject(path) + if _, err := object.Get(call); verror.ErrorID(err) != fs.ErrNotInMemStore.ID && !overwrite { + return verror.New(verror.ErrExist, ctx, "envelope already exists for profile", profile) + } + if _, err := object.Put(call, envelope); err != nil { + return verror.New(ErrOperationFailed, ctx) + } + if err := i.store.BindTransaction(tname).Commit(call); err != nil { + return verror.New(ErrOperationFailed, ctx) + } + return nil +} + +func (i *appRepoService) Remove(ctx *context.T, call rpc.ServerCall, profile string) error { + ctx.VI(0).Infof("%v.Remove(%v)", i.suffix, profile) + name, version, err := parse(ctx, i.suffix) + if err != nil { + return err + } + i.store.Lock() + defer i.store.Unlock() + // Transaction is rooted at "", so tname == tid. + tname, err := i.store.BindTransactionRoot("").CreateTransaction(call) + if err != nil { + return err + } + profiles := []string{profile} + if profile == "*" { + var err error + if profiles, err = i.store.BindObject(naming.Join("/applications", name)).Children(); err != nil { + return err + } + } + for _, profile := range profiles { + path := naming.Join(tname, "/applications", name, profile) + if version != "" { + path += "/" + version + } + object := i.store.BindObject(path) + found, err := object.Exists(call) + if err != nil { + return verror.New(ErrOperationFailed, ctx) + } + if !found { + return verror.New(verror.ErrNoExist, ctx) + } + if err := object.Remove(call); err != nil { + return verror.New(ErrOperationFailed, ctx) + } + } + if err := i.store.BindTransaction(tname).Commit(call); err != nil { + return verror.New(ErrOperationFailed, ctx) + } + return nil +} + +func (i *appRepoService) allApplications() ([]string, error) { + apps, err := i.store.BindObject("/applications").Children() + // There is no actual object corresponding to "/applications" in the + // store, so Children() returns ErrNoExist when there are no actual app + // objects under /applications. + if verror.ErrorID(err) == verror.ErrNoExist.ID { + return nil, nil + } + if err != nil { + return nil, err + } + return apps, nil +} + +func (i *appRepoService) allAppVersionsForProfiles(appName string, profiles []string) ([]string, error) { + uniqueVersions := make(map[string]struct{}) + for _, profile := range profiles { + versions, err := i.store.BindObject(naming.Join("/applications", appName, profile)).Children() + if verror.ErrorID(err) == verror.ErrNoExist.ID { + continue + } else if err != nil { + return nil, err + } + set.String.Union(uniqueVersions, set.String.FromSlice(versions)) + } + return set.String.ToSlice(uniqueVersions), nil +} + +func (i *appRepoService) allAppVersions(appName string) ([]string, error) { + profiles, err := i.store.BindObject(naming.Join("/applications", appName)).Children() + if err != nil { + return nil, err + } + return i.allAppVersionsForProfiles(appName, profiles) +} + +func (i *appRepoService) GlobChildren__(ctx *context.T, call rpc.GlobChildrenServerCall, m *glob.Element) error { + ctx.VI(0).Infof("%v.GlobChildren__()", i.suffix) + i.store.Lock() + defer i.store.Unlock() + + var elems []string + if i.suffix != "" { + elems = strings.Split(i.suffix, "/") + } + + var results []string + var err error + switch len(elems) { + case 0: + results, err = i.allApplications() + if err != nil { + return err + } + case 1: + results, err = i.allAppVersions(elems[0]) + if err != nil { + return err + } + case 2: + versions, err := i.allAppVersions(elems[0]) + if err != nil { + return err + } + for _, v := range versions { + if v == elems[1] { + return nil + } + } + return verror.New(verror.ErrNoExist, nil) + default: + return verror.New(verror.ErrNoExist, nil) + } + + for _, r := range results { + if m.Match(r) { + call.SendStream().Send(naming.GlobChildrenReplyName{Value: r}) + } + } + return nil +} + +func (i *appRepoService) GetPermissions(ctx *context.T, call rpc.ServerCall) (perms access.Permissions, version string, err error) { + name, _, err := parse(ctx, i.suffix) + if err != nil { + return nil, "", err + } + i.store.Lock() + defer i.store.Unlock() + path := naming.Join("/acls", name, "data") + + perms, version, err = getPermissions(ctx, i.store, path) + if verror.ErrorID(err) == verror.ErrNoExist.ID { + return pathperms.NilAuthPermissions(ctx, call.Security()), "", nil + } + + return perms, version, err +} + +func (i *appRepoService) SetPermissions(ctx *context.T, _ rpc.ServerCall, perms access.Permissions, version string) error { + name, _, err := parse(ctx, i.suffix) + if err != nil { + return err + } + i.store.Lock() + defer i.store.Unlock() + path := naming.Join("/acls", name, "data") + return setPermissions(ctx, i.store, path, perms, version) +} + +// getPermissions fetches a Permissions out of the Memstore at the provided path. +// path is expected to already have been cleaned by naming.Join or its ilk. +func getPermissions(ctx *context.T, store *fs.Memstore, path string) (access.Permissions, string, error) { + entry, err := store.BindObject(path).Get(nil) + + if verror.ErrorID(err) == fs.ErrNotInMemStore.ID { + // No Permissions exists + return nil, "", verror.New(verror.ErrNoExist, nil) + } else if err != nil { + ctx.Errorf("getPermissions: internal failure in fs.Memstore") + return nil, "", err + } + + perms, ok := entry.Value.(access.Permissions) + if !ok { + return nil, "", err + } + + version, err := pathperms.ComputeVersion(perms) + if err != nil { + return nil, "", err + } + return perms, version, nil +} + +// setPermissions writes a Permissions into the Memstore at the provided path. +// where path is expected to have already been cleaned by naming.Join. +func setPermissions(ctx *context.T, store *fs.Memstore, path string, perms access.Permissions, version string) error { + if version != "" { + _, oversion, err := getPermissions(ctx, store, path) + if verror.ErrorID(err) == verror.ErrNoExist.ID { + oversion = version + } else if err != nil { + return err + } + + if oversion != version { + return verror.NewErrBadVersion(nil) + } + } + + tname, err := store.BindTransactionRoot("").CreateTransaction(nil) + if err != nil { + return err + } + + object := store.BindObject(path) + + if _, err := object.Put(nil, perms); err != nil { + return err + } + if err := store.BindTransaction(tname).Commit(nil); err != nil { + return verror.New(ErrOperationFailed, nil) + } + return nil +} + +func (i *appRepoService) tidyRemoveVersions(call rpc.ServerCall, tname, appName, profile string, versions []string) error { + for _, v := range versions { + path := naming.Join(tname, "/applications", appName, profile, v) + object := i.store.BindObject(path) + if err := object.Remove(call); err != nil { + return err + } + } + return nil +} + +// numberOfVersionsToKeep can be set for tests. +var numberOfVersionsToKeep = 5 + +func (i *appRepoService) TidyNow(ctx *context.T, call rpc.ServerCall) error { + ctx.VI(2).Infof("%v.TidyNow()", i.suffix) + i.store.Lock() + defer i.store.Unlock() + + tname, err := i.store.BindTransactionRoot("").CreateTransaction(call) + if err != nil { + return err + } + + apps, err := i.allApplications() + if err != nil { + return err + } + + for _, app := range apps { + profiles, err := i.store.BindObject(naming.Join("/applications", app)).Children() + if err != nil { + return err + } + + for _, profile := range profiles { + versions, err := i.store.BindObject(naming.Join("/applications", app, profile)).Children() + if err != nil { + return err + } + + lv := len(versions) + if lv <= numberOfVersionsToKeep { + continue + } + + // Per assumption in Match, version names should ascend. + sort.Strings(versions) + versionsToRemove := versions[0 : lv-numberOfVersionsToKeep] + if err := i.tidyRemoveVersions(call, tname, app, profile, versionsToRemove); err != nil { + return err + } + } + } + + if err := i.store.BindTransaction(tname).Commit(call); err != nil { + return verror.New(ErrOperationFailed, ctx) + } + return nil + +} diff --git a/x/ref/services/application/applicationd/v23_test.go b/x/ref/services/application/applicationd/v23_test.go new file mode 100644 index 000000000..e0f0e673d --- /dev/null +++ b/x/ref/services/application/applicationd/v23_test.go @@ -0,0 +1,15 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "testing" + + "v.io/x/ref/test/v23test" +) + +func TestMain(m *testing.M) { + v23test.TestMain(m) +} diff --git a/x/ref/services/binary/binary/doc.go b/x/ref/services/binary/binary/doc.go new file mode 100644 index 000000000..6e88f097b --- /dev/null +++ b/x/ref/services/binary/binary/doc.go @@ -0,0 +1,151 @@ +// Copyright 2018 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated via go generate. +// DO NOT UPDATE MANUALLY + +/* +Command binary manages the Vanadium binary repository. + +Usage: + binary [flags] <command> + +The binary commands are: + delete Delete a binary + download Download a binary + upload Upload a binary or directory archive + url Fetch a download URL + help Display help for commands or topics + +The global flags are: + -alsologtostderr=true + log to standard error as well as files + -log_backtrace_at=:0 + when logging hits line file:N, emit a stack trace + -log_dir= + if non-empty, write log files to this directory + -logtostderr=false + log to standard error instead of files + -max_stack_buf_size=4292608 + max size in bytes of the buffer to use for logging stack traces + -metadata=<just specify -metadata to activate> + Displays metadata for the program and exits. + -stderrthreshold=2 + logs at or above this threshold go to stderr + -time=false + Dump timing information to stderr before exiting the program. + -v=0 + log level for V logs + -v23.credentials= + directory to use for storing security credentials + -v23.i18n-catalogue= + 18n catalogue files to load, comma separated + -v23.namespace.root=[/(dev.v.io:r:vprod:service:mounttabled)@ns.dev.v.io:8101] + local namespace root; can be repeated to provided multiple roots + -v23.permissions.file= + specify a perms file as <name>:<permsfile> + -v23.permissions.literal= + explicitly specify the runtime perms as a JSON-encoded access.Permissions. + Overrides all --v23.permissions.file flags + -v23.proxy= + object name of proxy service to use to export services across network + boundaries + -v23.tcp.address= + address to listen on + -v23.tcp.protocol= + protocol to listen with + -v23.vtrace.cache-size=1024 + The number of vtrace traces to store in memory + -v23.vtrace.collect-regexp= + Spans and annotations that match this regular expression will trigger trace + collection + -v23.vtrace.dump-on-shutdown=true + If true, dump all stored traces on runtime shutdown + -v23.vtrace.sample-rate=0 + Rate (from 0.0 to 1.0) to sample vtrace traces + -v23.vtrace.v=0 + The verbosity level of the log messages to be captured in traces + -vmodule= + comma-separated list of globpattern=N settings for filename-filtered logging + (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns baz or + *az or b* but not by bar/baz or baz.go or az or b.* + -vpath= + comma-separated list of regexppattern=N settings for file pathname-filtered + logging (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns + foo/bar/baz or fo.*az or oo/ba or b.z but not by foo/bar/baz.go or fo*az + +Binary delete - Delete a binary + +Delete connects to the binary repository and deletes the specified binary + +Usage: + binary delete [flags] <von> + +<von> is the vanadium object name of the binary to delete + +Binary download - Download a binary + +Download connects to the binary repository, downloads the specified binary, and +installs it to the specified location. + +Usage: + binary download [flags] <von> <location> + +<von> is the vanadium object name of the binary to download <location> is the +path where the downloaded binary should be installed + +The binary download flags are: + -install=true + Install the binary. If false, it just downloads the binary and the media + info file. + +Binary upload - Upload a binary or directory archive + +Upload connects to the binary repository and uploads the binary of the specified +file or archive of the specified directory. When successful, it writes the name +of the new binary to stdout. + +Usage: + binary upload [flags] <von> <filename> + +<von> is the vanadium object name of the binary to upload <filename> is the name +of the file or directory to upload + +Binary url - Fetch a download URL + +Connect to the binary repository and fetch the download URL for the given +vanadium object name. + +Usage: + binary url [flags] <von> + +<von> is the vanadium object name of the binary repository + +Binary help - Display help for commands or topics + +Help with no args displays the usage of the parent command. + +Help with args displays the usage of the specified sub-command or help topic. + +"help ..." recursively displays help for all commands and topics. + +Usage: + binary help [flags] [command/topic ...] + +[command/topic ...] optionally identifies a specific sub-command or help topic. + +The binary help flags are: + -style=compact + The formatting style for help output: + compact - Good for compact cmdline output. + full - Good for cmdline output, shows all global flags. + godoc - Good for godoc processing. + shortonly - Only output short description. + Override the default by setting the CMDLINE_STYLE environment variable. + -width=<terminal width> + Format output to this target width in runes, or unlimited if width < 0. + Defaults to the terminal width if available. Override the default by setting + the CMDLINE_WIDTH environment variable. +*/ +package main diff --git a/x/ref/services/binary/binary/impl.go b/x/ref/services/binary/binary/impl.go new file mode 100644 index 000000000..c2d553612 --- /dev/null +++ b/x/ref/services/binary/binary/impl.go @@ -0,0 +1,173 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The following enables go generate to generate the doc.go file. +//go:generate gendoc . + +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "v.io/v23/context" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + _ "v.io/x/ref/runtime/factories/generic" + "v.io/x/ref/services/internal/binarylib" + "v.io/x/ref/services/internal/packages" +) + +func main() { + cmdline.HideGlobalFlagsExcept() + cmdline.Main(cmdRoot) +} + +var cmdDelete = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runDelete), + Name: "delete", + Short: "Delete a binary", + Long: "Delete connects to the binary repository and deletes the specified binary", + ArgsName: "<von>", + ArgsLong: "<von> is the vanadium object name of the binary to delete", +} + +func runDelete(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 1, len(args); expected != got { + return env.UsageErrorf("delete: incorrect number of arguments, expected %d, got %d", expected, got) + } + von := args[0] + if err := binarylib.Delete(ctx, von); err != nil { + return err + } + fmt.Fprintf(env.Stdout, "Binary deleted successfully\n") + return nil +} + +var install bool + +var cmdDownload = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runDownload), + Name: "download", + Short: "Download a binary", + Long: ` +Download connects to the binary repository, downloads the specified binary, and +installs it to the specified location. +`, + ArgsName: "<von> <location>", + ArgsLong: ` +<von> is the vanadium object name of the binary to download +<location> is the path where the downloaded binary should be installed +`, +} + +func init() { + cmdDownload.Flags.BoolVar(&install, "install", true, "Install the binary. If false, it just downloads the binary and the media info file.") +} + +func runDownload(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 2, len(args); expected != got { + return env.UsageErrorf("download: incorrect number of arguments, expected %d, got %d", expected, got) + } + von, destination, rawDestination := args[0], args[1], args[1] + if install { + rawDestinationFile, err := ioutil.TempFile(filepath.Dir(destination), filepath.Base(destination)) + if err != nil { + return err + } + rawDestination = rawDestinationFile.Name() + rawDestinationFile.Close() + } + if err := binarylib.DownloadToFile(ctx, von, rawDestination); err != nil { + return err + } + if !install { + fmt.Fprintf(env.Stdout, "Binary downloaded to %s (media info %s)\n", rawDestination, packages.MediaInfoFile(rawDestination)) + return nil + } + if err := packages.Install(rawDestination, destination); err != nil { + return err + } + if err := os.Remove(rawDestination); err != nil { + return err + } + if err := os.Remove(packages.MediaInfoFile(rawDestination)); err != nil { + return err + } + fmt.Fprintf(env.Stdout, "Binary installed as %s\n", destination) + return nil +} + +var cmdUpload = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runUpload), + Name: "upload", + Short: "Upload a binary or directory archive", + Long: ` +Upload connects to the binary repository and uploads the binary of the specified +file or archive of the specified directory. When successful, it writes the name of the new binary to stdout. +`, + ArgsName: "<von> <filename>", + ArgsLong: ` +<von> is the vanadium object name of the binary to upload +<filename> is the name of the file or directory to upload +`, +} + +func runUpload(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 2, len(args); expected != got { + return env.UsageErrorf("upload: incorrect number of arguments, expected %d, got %d", expected, got) + } + von, filename := args[0], args[1] + fi, err := os.Stat(filename) + if err != nil { + return err + } + if fi.IsDir() { + sig, err := binarylib.UploadFromDir(ctx, von, filename) + if err != nil { + return err + } + fmt.Fprintf(env.Stdout, "Binary package uploaded from directory %s signature(%v)\n", filename, sig) + return nil + } + sig, err := binarylib.UploadFromFile(ctx, von, filename) + if err != nil { + return err + } + fmt.Fprintf(env.Stdout, "Binary uploaded from file %s signature(%v)\n", filename, sig) + return nil +} + +var cmdURL = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runURL), + Name: "url", + Short: "Fetch a download URL", + Long: "Connect to the binary repository and fetch the download URL for the given vanadium object name.", + ArgsName: "<von>", + ArgsLong: "<von> is the vanadium object name of the binary repository", +} + +func runURL(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 1, len(args); expected != got { + return env.UsageErrorf("rooturl: incorrect number of arguments, expected %d, got %d", expected, got) + } + von := args[0] + url, _, err := binarylib.DownloadUrl(ctx, von) + if err != nil { + return err + } + fmt.Fprintf(env.Stdout, "%v\n", url) + return nil +} + +var cmdRoot = &cmdline.Command{ + Name: "binary", + Short: "manages the Vanadium binary repository", + Long: ` +Command binary manages the Vanadium binary repository. +`, + Children: []*cmdline.Command{cmdDelete, cmdDownload, cmdUpload, cmdURL}, +} diff --git a/x/ref/services/binary/binary/impl_test.go b/x/ref/services/binary/binary/impl_test.go new file mode 100644 index 000000000..96eab327f --- /dev/null +++ b/x/ref/services/binary/binary/impl_test.go @@ -0,0 +1,160 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "fmt" + "io/ioutil" + "os" + "path" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/binary" + "v.io/v23/services/repository" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + _ "v.io/x/ref/runtime/factories/generic" + "v.io/x/ref/test" +) + +type server struct { + suffix string +} + +func (s *server) Create(ctx *context.T, _ rpc.ServerCall, _ int32, _ repository.MediaInfo) error { + ctx.Infof("Create() was called. suffix=%v", s.suffix) + return nil +} + +func (s *server) Delete(ctx *context.T, _ rpc.ServerCall) error { + ctx.Infof("Delete() was called. suffix=%v", s.suffix) + if s.suffix != "exists" { + return fmt.Errorf("binary doesn't exist: %v", s.suffix) + } + return nil +} + +func (s *server) Download(ctx *context.T, call repository.BinaryDownloadServerCall, _ int32) error { + ctx.Infof("Download() was called. suffix=%v", s.suffix) + sender := call.SendStream() + sender.Send([]byte("Hello")) + sender.Send([]byte("World")) + return nil +} + +func (s *server) DownloadUrl(ctx *context.T, _ rpc.ServerCall) (string, int64, error) { + ctx.Infof("DownloadUrl() was called. suffix=%v", s.suffix) + if s.suffix != "" { + return "", 0, fmt.Errorf("non-empty suffix: %v", s.suffix) + } + return "test-download-url", 0, nil +} + +func (s *server) Stat(ctx *context.T, _ rpc.ServerCall) ([]binary.PartInfo, repository.MediaInfo, error) { + ctx.Infof("Stat() was called. suffix=%v", s.suffix) + h := md5.New() + text := "HelloWorld" + h.Write([]byte(text)) + part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(text))} + return []binary.PartInfo{part}, repository.MediaInfo{Type: "text/plain"}, nil +} + +func (s *server) Upload(ctx *context.T, call repository.BinaryUploadServerCall, _ int32) error { + ctx.Infof("Upload() was called. suffix=%v", s.suffix) + rStream := call.RecvStream() + for rStream.Advance() { + } + return nil +} + +func (s *server) GetPermissions(ctx *context.T, _ rpc.ServerCall) (perms access.Permissions, version string, err error) { + return nil, "", nil +} + +func (s *server) SetPermissions(ctx *context.T, _ rpc.ServerCall, perms access.Permissions, version string) error { + return nil +} + +type dispatcher struct { +} + +func NewDispatcher() rpc.Dispatcher { + return &dispatcher{} +} + +func (d *dispatcher) Lookup(_ *context.T, suffix string) (interface{}, security.Authorizer, error) { + return repository.BinaryServer(&server{suffix: suffix}), nil, nil +} + +func TestBinaryClient(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + _, server, err := v23.WithNewDispatchingServer(ctx, "", NewDispatcher()) + if err != nil { + t.Fatalf("NewServer failed: %v", err) + } + endpoint := server.Status().Endpoints[0] + + // Setup the command-line. + var out bytes.Buffer + env := &cmdline.Env{Stdout: &out, Stderr: &out} + + // Test the 'delete' command. + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"delete", naming.JoinAddressName(endpoint.String(), "exists")}); err != nil { + t.Fatalf("%v failed: %v\n%v", "delete", err, out.String()) + } + if expected, got := "Binary deleted successfully", strings.TrimSpace(out.String()); got != expected { + t.Errorf("Got %q, expected %q", got, expected) + } + out.Reset() + + // Test the 'download' command. + dir, err := ioutil.TempDir("", "binaryimpltest") + if err != nil { + t.Fatalf("%v", err) + } + defer os.RemoveAll(dir) + file := path.Join(dir, "testfile") + defer os.Remove(file) + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"download", naming.JoinAddressName(endpoint.String(), "exists"), file}); err != nil { + t.Fatalf("%v failed: %v\n%v", "download", err, out.String()) + } + if expected, got := "Binary installed as "+file, strings.TrimSpace(out.String()); got != expected { + t.Errorf("Got %q, expected %q", got, expected) + } + buf, err := ioutil.ReadFile(file) + if err != nil { + t.Fatalf("%v", err) + } + if expected := "HelloWorld"; string(buf) != expected { + t.Errorf("Got %q, expected %q", string(buf), expected) + } + out.Reset() + + // Test the 'upload' command. + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"upload", naming.JoinAddressName(endpoint.String(), "exists"), file}); err != nil { + t.Fatalf("%v failed: %v\n%v", "upload", err, out.String()) + } + out.Reset() + + // Test the 'url' command. + if err := v23cmd.ParseAndRunForTest(cmdRoot, ctx, env, []string{"url", naming.JoinAddressName(endpoint.String(), "")}); err != nil { + t.Fatalf("%v failed: %v\n%v", "url", err, out.String()) + } + if expected, got := "test-download-url", strings.TrimSpace(out.String()); got != expected { + t.Errorf("Got %q, expected %q", got, expected) + } +} diff --git a/x/ref/services/binary/binaryd/binaryd_v23_test.go b/x/ref/services/binary/binaryd/binaryd_v23_test.go new file mode 100644 index 000000000..27df0f491 --- /dev/null +++ b/x/ref/services/binary/binaryd/binaryd_v23_test.go @@ -0,0 +1,220 @@ +// Copyright 2015 The Vanadium 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 main_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "testing" + + "v.io/v23/naming" + "v.io/x/ref/test/testutil" + "v.io/x/ref/test/v23test" +) + +func checkFileType(t *testing.T, infoFile, typeString string) { + var catOut bytes.Buffer + catCmd := exec.Command("cat", infoFile) + catCmd.Stdout = &catOut + catCmd.Stderr = &catOut + if err := catCmd.Run(); err != nil { + t.Fatalf("%q failed: %v\n%v", strings.Join(catCmd.Args, " "), err, catOut.String()) + } + if got, want := strings.TrimSpace(catOut.String()), typeString; got != want { + t.Fatalf("unexpect file type: got %v, want %v", got, want) + } +} + +func readFileOrDie(t *testing.T, path string) []byte { + result, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("ReadFile(%q) failed: %v", path, err) + } + return result +} + +func compareFiles(t *testing.T, f1, f2 string) { + if !bytes.Equal(readFileOrDie(t, f1), readFileOrDie(t, f2)) { + t.Fatalf("the contents of %s and %s differ when they should not", f1, f2) + } +} + +func deleteFile(t *testing.T, sh *v23test.Shell, creds *v23test.Credentials, bin, name, suffix string) { + args := []string{"delete", naming.Join(name, suffix)} + sh.Cmd(bin, args...).WithCredentials(creds).Run() +} + +func downloadAndInstall(t *testing.T, sh *v23test.Shell, creds *v23test.Credentials, bin, name, path, suffix string) { + args := []string{"download", naming.Join(name, suffix), path} + sh.Cmd(bin, args...).WithCredentials(creds).Run() +} + +func downloadFile(t *testing.T, sh *v23test.Shell, creds *v23test.Credentials, bin, name, path, suffix string) (string, string) { + args := []string{"download", "--install=false", naming.Join(name, suffix), path} + stdout := sh.Cmd(bin, args...).WithCredentials(creds).Stdout() + match := regexp.MustCompile(`Binary downloaded to (.+) \(media info (.+)\)`).FindStringSubmatch(stdout) + if len(match) != 3 { + t.Fatalf("Failed to match download stdout: %s", stdout) + } + return match[1], match[2] +} + +func downloadFileExpectError(t *testing.T, sh *v23test.Shell, creds *v23test.Credentials, bin, name, path, suffix string) { + args := []string{"download", naming.Join(name, suffix), path} + cmd := sh.Cmd(bin, args...).WithCredentials(creds) + cmd.ExitErrorIsOk = true + if cmd.Run(); cmd.Err == nil { + t.Fatalf("%s %v: did not fail when it should", bin, args) + } +} + +func downloadURL(t *testing.T, path, rootURL, suffix string) { + url := fmt.Sprintf("http://%v/%v", rootURL, suffix) + resp, err := http.Get(url) + if err != nil { + t.Fatalf("Get(%q) failed: %v", url, err) + } + output, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + t.Fatalf("ReadAll() failed: %v", err) + } + if err = ioutil.WriteFile(path, output, 0600); err != nil { + t.Fatalf("WriteFile() failed: %v", err) + } +} + +func rootURL(t *testing.T, sh *v23test.Shell, creds *v23test.Credentials, bin, name string) string { + args := []string{"url", name} + stdout := sh.Cmd(bin, args...).WithCredentials(creds).Stdout() + return strings.TrimSpace(stdout) +} + +func uploadFile(t *testing.T, sh *v23test.Shell, creds *v23test.Credentials, bin, name, path, suffix string) { + args := []string{"upload", naming.Join(name, suffix), path} + sh.Cmd(bin, args...).WithCredentials(creds).Run() +} + +func TestV23BinaryRepositoryIntegration(t *testing.T) { + v23test.SkipUnlessRunningIntegrationTests(t) + sh := v23test.NewShell(t, nil) + defer sh.Cleanup() + sh.StartRootMountTable() + + testutil.InitRandGenerator(t.Logf) + + // Build the required binaries. + // The client must run as a "delegate" of the server in order to pass + // the default authorization checks on the server. + var ( + binaryRepoBin = v23test.BuildGoPkg(sh, "v.io/x/ref/services/binary/binaryd") + clientBin = v23test.BuildGoPkg(sh, "v.io/x/ref/services/binary/binary") + binaryRepoCreds = sh.ForkCredentials("binaryd") + clientCreds = sh.ForkCredentials("binaryd:client") + ) + + // Start the build server. + binaryRepoName := "test-binary-repository" + sh.Cmd(binaryRepoBin, + "-name="+binaryRepoName, + "-http=127.0.0.1:0", + "-v23.tcp.address=127.0.0.1:0").WithCredentials(binaryRepoCreds).Start() + + // Upload a random binary file. + binFile := sh.MakeTempFile() + if _, err := binFile.Write(testutil.RandomBytes(16 * 1000 * 1000)); err != nil { + t.Fatalf("Write() failed: %v", err) + } + binSuffix := "test-binary" + uploadFile(t, sh, clientCreds, clientBin, binaryRepoName, binFile.Name(), binSuffix) + + // Upload a compressed version of the binary file. + tarFile := binFile.Name() + ".tar.gz" + var tarOut bytes.Buffer + tarCmd := exec.Command("tar", "zcvf", tarFile, binFile.Name()) + tarCmd.Stdout = &tarOut + tarCmd.Stderr = &tarOut + if err := tarCmd.Run(); err != nil { + t.Fatalf("%q failed: %v\n%v", strings.Join(tarCmd.Args, " "), err, tarOut.String()) + } + defer os.Remove(tarFile) + tarSuffix := "test-compressed-file" + uploadFile(t, sh, clientCreds, clientBin, binaryRepoName, tarFile, tarSuffix) + + // Download the binary file and check that it matches the + // original one and that it has the right file type. + downloadName := binFile.Name() + "-downloaded" + downloadedFile, infoFile := downloadFile(t, sh, clientCreds, clientBin, binaryRepoName, downloadName, binSuffix) + defer os.Remove(downloadedFile) + defer os.Remove(infoFile) + if downloadedFile != downloadName { + t.Fatalf("expected %s, got %s", downloadName, downloadedFile) + } + compareFiles(t, binFile.Name(), downloadedFile) + checkFileType(t, infoFile, `{"Type":"application/octet-stream","Encoding":""}`) + + // Download and install and make sure the file is as expected. + installName := binFile.Name() + "-installed" + downloadAndInstall(t, sh, clientCreds, clientBin, binaryRepoName, installName, binSuffix) + defer os.Remove(installName) + compareFiles(t, binFile.Name(), installName) + + // Download the compressed version of the binary file and + // check that it matches the original one and that it has the + // right file type. + downloadTarName := binFile.Name() + "-compressed-downloaded" + downloadedTarFile, infoFile := downloadFile(t, sh, clientCreds, clientBin, binaryRepoName, downloadTarName, tarSuffix) + defer os.Remove(downloadedTarFile) + defer os.Remove(infoFile) + if downloadedTarFile != downloadTarName { + t.Fatalf("expected %s, got %s", downloadTarName, downloadedTarFile) + } + compareFiles(t, tarFile, downloadedTarFile) + checkFileType(t, infoFile, `{"Type":"application/x-tar","Encoding":"gzip"}`) + + // Download and install and make sure the un-archived file is as expected. + installTarName := binFile.Name() + "-compressed-installed" + downloadAndInstall(t, sh, clientCreds, clientBin, binaryRepoName, installTarName, tarSuffix) + defer os.Remove(installTarName) + compareFiles(t, binFile.Name(), filepath.Join(installTarName, binFile.Name())) + + // Fetch the root URL of the HTTP server used by the binary + // repository to serve URLs. + root := rootURL(t, sh, clientCreds, clientBin, binaryRepoName) + + // Download the binary file using the HTTP protocol and check + // that it matches the original one. + downloadedBinFileURL := binFile.Name() + "-downloaded-url" + defer os.Remove(downloadedBinFileURL) + downloadURL(t, downloadedBinFileURL, root, binSuffix) + compareFiles(t, downloadName, downloadedBinFileURL) + + // Download the compressed version of the binary file using + // the HTTP protocol and check that it matches the original + // one. + downloadedTarFileURL := binFile.Name() + "-downloaded-url.tar.gz" + defer os.Remove(downloadedTarFileURL) + downloadURL(t, downloadedTarFileURL, root, tarSuffix) + compareFiles(t, downloadedTarFile, downloadedTarFileURL) + + // Delete the files. + deleteFile(t, sh, clientCreds, clientBin, binaryRepoName, binSuffix) + deleteFile(t, sh, clientCreds, clientBin, binaryRepoName, tarSuffix) + + // Check the files no longer exist. + downloadFileExpectError(t, sh, clientCreds, clientBin, binaryRepoName, downloadName, binSuffix) + downloadFileExpectError(t, sh, clientCreds, clientBin, binaryRepoName, downloadTarName, tarSuffix) +} + +func TestMain(m *testing.M) { + v23test.TestMain(m) +} diff --git a/x/ref/services/binary/binaryd/doc.go b/x/ref/services/binary/binaryd/doc.go new file mode 100644 index 000000000..052b23892 --- /dev/null +++ b/x/ref/services/binary/binaryd/doc.go @@ -0,0 +1,80 @@ +// Copyright 2018 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated via go generate. +// DO NOT UPDATE MANUALLY + +/* +Command binaryd runs the binary daemon, which implements the +v.io/v23/services/repository.Binary interface. + +Usage: + binaryd [flags] + +The binaryd flags are: + -http=:0 + TCP address on which the HTTP server runs. + -name= + Name to mount the binary repository as. + -root-dir= + Root directory for the binary repository. + +The global flags are: + -alsologtostderr=true + log to standard error as well as files + -log_backtrace_at=:0 + when logging hits line file:N, emit a stack trace + -log_dir= + if non-empty, write log files to this directory + -logtostderr=false + log to standard error instead of files + -max_stack_buf_size=4292608 + max size in bytes of the buffer to use for logging stack traces + -metadata=<just specify -metadata to activate> + Displays metadata for the program and exits. + -stderrthreshold=2 + logs at or above this threshold go to stderr + -time=false + Dump timing information to stderr before exiting the program. + -v=0 + log level for V logs + -v23.credentials= + directory to use for storing security credentials + -v23.i18n-catalogue= + 18n catalogue files to load, comma separated + -v23.namespace.root=[/(dev.v.io:r:vprod:service:mounttabled)@ns.dev.v.io:8101] + local namespace root; can be repeated to provided multiple roots + -v23.permissions.file= + specify a perms file as <name>:<permsfile> + -v23.permissions.literal= + explicitly specify the runtime perms as a JSON-encoded access.Permissions. + Overrides all --v23.permissions.file flags + -v23.proxy= + object name of proxy service to use to export services across network + boundaries + -v23.tcp.address= + address to listen on + -v23.tcp.protocol= + protocol to listen with + -v23.vtrace.cache-size=1024 + The number of vtrace traces to store in memory + -v23.vtrace.collect-regexp= + Spans and annotations that match this regular expression will trigger trace + collection + -v23.vtrace.dump-on-shutdown=true + If true, dump all stored traces on runtime shutdown + -v23.vtrace.sample-rate=0 + Rate (from 0.0 to 1.0) to sample vtrace traces + -v23.vtrace.v=0 + The verbosity level of the log messages to be captured in traces + -vmodule= + comma-separated list of globpattern=N settings for filename-filtered logging + (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns baz or + *az or b* but not by bar/baz or baz.go or az or b.* + -vpath= + comma-separated list of regexppattern=N settings for file pathname-filtered + logging (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns + foo/bar/baz or fo.*az or oo/ba or b.z but not by foo/bar/baz.go or fo*az +*/ +package main diff --git a/x/ref/services/binary/binaryd/main.go b/x/ref/services/binary/binaryd/main.go new file mode 100644 index 000000000..476583f42 --- /dev/null +++ b/x/ref/services/binary/binaryd/main.go @@ -0,0 +1,113 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The following enables go generate to generate the doc.go file. +//go:generate gendoc . -help + +package main + +import ( + "fmt" + "net" + "net/http" + "os" + + "v.io/v23" + "v.io/v23/context" + "v.io/x/lib/cmdline" + "v.io/x/lib/netstate" + "v.io/x/ref/lib/signals" + "v.io/x/ref/lib/v23cmd" + _ "v.io/x/ref/runtime/factories/roaming" + "v.io/x/ref/services/internal/binarylib" +) + +const defaultDepth = 3 + +var name, rootDirFlag, httpAddr string + +func main() { + cmdBinaryD.Flags.StringVar(&name, "name", "", "Name to mount the binary repository as.") + cmdBinaryD.Flags.StringVar(&rootDirFlag, "root-dir", "", "Root directory for the binary repository.") + cmdBinaryD.Flags.StringVar(&httpAddr, "http", ":0", "TCP address on which the HTTP server runs.") + + cmdline.HideGlobalFlagsExcept() + cmdline.Main(cmdBinaryD) +} + +var cmdBinaryD = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runBinaryD), + Name: "binaryd", + Short: "Runs the binary daemon.", + Long: ` +Command binaryd runs the binary daemon, which implements the +v.io/v23/services/repository.Binary interface. +`, +} + +// toIPPort tries to swap in the 'best' accessible IP for the host part of the +// address, if the provided address has an unspecified IP. +func toIPPort(ctx *context.T, addr string) string { + // TODO(caprita): consider using netstate.PossibleAddresses() + host, port, err := net.SplitHostPort(addr) + if err != nil { + ctx.Errorf("SplitHostPort(%v) failed: %v", addr, err) + os.Exit(1) + } + ip := net.ParseIP(host) + if ip.IsUnspecified() { + host = "127.0.0.1" + ips, err := netstate.GetAccessibleIPs() + if err == nil { + ls := v23.GetListenSpec(ctx) + if a, err := ls.AddressChooser.ChooseAddresses("tcp", ips.AsNetAddrs()); err == nil && len(a) > 0 { + host = a[0].String() + } + } + } + return net.JoinHostPort(host, port) +} + +func runBinaryD(ctx *context.T, env *cmdline.Env, args []string) error { + rootDir, err := binarylib.SetupRootDir(rootDirFlag) + if err != nil { + return fmt.Errorf("SetupRootDir(%q) failed: %v", rootDirFlag, err) + } + ctx.Infof("Binary repository rooted at %v", rootDir) + + listener, err := net.Listen("tcp", httpAddr) + if err != nil { + return fmt.Errorf("Listen(%s) failed: %v", httpAddr, err) + } + rootURL := toIPPort(ctx, listener.Addr().String()) + state, err := binarylib.NewState(rootDir, rootURL, defaultDepth) + if err != nil { + return fmt.Errorf("NewState(%v, %v, %v) failed: %v", rootDir, rootURL, defaultDepth, err) + } + ctx.Infof("Binary repository HTTP server at: %q", rootURL) + go func() { + if err := http.Serve(listener, http.FileServer(binarylib.NewHTTPRoot(ctx, state))); err != nil { + ctx.Errorf("Serve() failed: %v", err) + os.Exit(1) + } + }() + + dis, err := binarylib.NewDispatcher(ctx, state) + if err != nil { + return fmt.Errorf("NewDispatcher() failed: %v\n", err) + } + ctx, server, err := v23.WithNewDispatchingServer(ctx, name, dis) + if err != nil { + return fmt.Errorf("NewServer() failed: %v", err) + } + epName := server.Status().Endpoints[0].Name() + if name != "" { + ctx.Infof("Binary repository serving at %q (%q)", name, epName) + } else { + ctx.Infof("Binary repository serving at %q", epName) + } + // Wait until shutdown. + <-signals.ShutdownOnSignals(ctx) + return nil +} diff --git a/x/ref/services/binary/tidy/appd/mock.go b/x/ref/services/binary/tidy/appd/mock.go new file mode 100644 index 000000000..d401402d9 --- /dev/null +++ b/x/ref/services/binary/tidy/appd/mock.go @@ -0,0 +1,55 @@ +// Copyright 2015 The Vanadium 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 appd + +import ( + "testing" + + "v.io/v23/context" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/services/application" + + "v.io/x/ref/services/binary/tidy/binaryd" + "v.io/x/ref/services/internal/servicetest" +) + +type mockAppdInvoker struct { + binaryd.MockBinarydInvoker +} + +type MatchStimulus struct { + Name string + Suffix string + Profiles []string +} + +type MatchResult struct { + Env application.Envelope + Err error +} + +func (mdi *mockAppdInvoker) Match(ctx *context.T, _ rpc.ServerCall, profiles []string) (application.Envelope, error) { + ir := mdi.Tape.Record(MatchStimulus{"Match", mdi.Suffix, profiles}) + r := ir.(MatchResult) + return r.Env, r.Err +} + +func (mdi *mockAppdInvoker) TidyNow(ctx *context.T, _ rpc.ServerCall) error { + return mdi.SimpleCore("TidyNow", "TidyNow") +} + +type dispatcher struct { + tape *servicetest.Tape + t *testing.T +} + +func NewDispatcher(t *testing.T, tape *servicetest.Tape) rpc.Dispatcher { + return &dispatcher{tape: tape, t: t} +} + +func (d *dispatcher) Lookup(p *context.T, suffix string) (interface{}, security.Authorizer, error) { + return &mockAppdInvoker{binaryd.NewMockBinarydInvoker(suffix, d.tape, d.t)}, nil, nil +} diff --git a/x/ref/services/binary/tidy/binaryd/mock.go b/x/ref/services/binary/tidy/binaryd/mock.go new file mode 100644 index 000000000..d3efd8cc4 --- /dev/null +++ b/x/ref/services/binary/tidy/binaryd/mock.go @@ -0,0 +1,98 @@ +// Copyright 2015 The Vanadium 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 binaryd + +import ( + "log" + "testing" + + "v.io/v23/context" + "v.io/v23/glob" + "v.io/v23/naming" + "v.io/v23/rpc" + "v.io/v23/security" + "v.io/v23/services/binary" + "v.io/v23/services/repository" + + "v.io/x/ref/services/internal/servicetest" +) + +type MockBinarydInvoker struct { + Suffix string + Tape *servicetest.Tape + t *testing.T +} + +// simpleCore implements the core of all mock methods that take +// arguments and return error. +func (mdi *MockBinarydInvoker) SimpleCore(callRecord interface{}, name string) error { + ri := mdi.Tape.Record(callRecord) + switch r := ri.(type) { + case nil: + return nil + case error: + return r + } + log.Fatalf("%s (mock) response %v is of bad type", name, ri) + return nil +} + +type DeleteStimulus struct { + Op string + Suffix string +} + +func (mdi *MockBinarydInvoker) Delete(ctx *context.T, _ rpc.ServerCall) error { + return mdi.SimpleCore(DeleteStimulus{"Delete", mdi.Suffix}, "Delete") +} + +type StatStimulus struct { + Op string + Suffix string +} + +func (mdi *MockBinarydInvoker) Stat(ctx *context.T, _ rpc.ServerCall) ([]binary.PartInfo, repository.MediaInfo, error) { + // Only the presence or absence of the error is necessary. + if err := mdi.SimpleCore(StatStimulus{"Stat", mdi.Suffix}, "Stat"); err != nil { + return nil, repository.MediaInfo{}, err + } + return nil, repository.MediaInfo{}, nil +} + +type GlobStimulus struct { + Pattern string +} + +type GlobResponse struct { + Results []string + Err error +} + +func (mdi *MockBinarydInvoker) Glob__(p *context.T, call rpc.GlobServerCall, g *glob.Glob) error { + gs := GlobStimulus{g.String()} + gr := mdi.Tape.Record(gs).(GlobResponse) + for _, r := range gr.Results { + call.SendStream().Send(naming.GlobReplyEntry{Value: naming.MountEntry{Name: r}}) + } + return gr.Err +} + +type dispatcher struct { + tape *servicetest.Tape + t *testing.T +} + +func NewDispatcher(t *testing.T, tape *servicetest.Tape) rpc.Dispatcher { + return &dispatcher{tape: tape, t: t} +} + +func NewMockBinarydInvoker(suffix string, tape *servicetest.Tape, t *testing.T) MockBinarydInvoker { + return MockBinarydInvoker{Suffix: suffix, Tape: tape, t: t} +} + +func (d *dispatcher) Lookup(p *context.T, suffix string) (interface{}, security.Authorizer, error) { + v := NewMockBinarydInvoker(suffix, d.tape, d.t) + return &v, nil, nil +} diff --git a/x/ref/services/binary/tidy/doc.go b/x/ref/services/binary/tidy/doc.go new file mode 100644 index 000000000..a9f2d6871 --- /dev/null +++ b/x/ref/services/binary/tidy/doc.go @@ -0,0 +1,124 @@ +// Copyright 2018 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file was auto-generated via go generate. +// DO NOT UPDATE MANUALLY + +/* +Tidy tidies the Vanadium repository by removing unused envelopes and binaries. + +Usage: + tidy [flags] <command> + +The tidy commands are: + binary Binary sub-command tidies a specified binaryd + application Application sub-command tidies a specified applicationd + help Display help for commands or topics + +The global flags are: + -alsologtostderr=true + log to standard error as well as files + -log_backtrace_at=:0 + when logging hits line file:N, emit a stack trace + -log_dir= + if non-empty, write log files to this directory + -logtostderr=false + log to standard error instead of files + -max_stack_buf_size=4292608 + max size in bytes of the buffer to use for logging stack traces + -metadata=<just specify -metadata to activate> + Displays metadata for the program and exits. + -stderrthreshold=2 + logs at or above this threshold go to stderr + -time=false + Dump timing information to stderr before exiting the program. + -v=0 + log level for V logs + -v23.credentials= + directory to use for storing security credentials + -v23.i18n-catalogue= + 18n catalogue files to load, comma separated + -v23.namespace.root=[/(dev.v.io:r:vprod:service:mounttabled)@ns.dev.v.io:8101] + local namespace root; can be repeated to provided multiple roots + -v23.permissions.file= + specify a perms file as <name>:<permsfile> + -v23.permissions.literal= + explicitly specify the runtime perms as a JSON-encoded access.Permissions. + Overrides all --v23.permissions.file flags + -v23.proxy= + object name of proxy service to use to export services across network + boundaries + -v23.tcp.address= + address to listen on + -v23.tcp.protocol= + protocol to listen with + -v23.vtrace.cache-size=1024 + The number of vtrace traces to store in memory + -v23.vtrace.collect-regexp= + Spans and annotations that match this regular expression will trigger trace + collection + -v23.vtrace.dump-on-shutdown=true + If true, dump all stored traces on runtime shutdown + -v23.vtrace.sample-rate=0 + Rate (from 0.0 to 1.0) to sample vtrace traces + -v23.vtrace.v=0 + The verbosity level of the log messages to be captured in traces + -vmodule= + comma-separated list of globpattern=N settings for filename-filtered logging + (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns baz or + *az or b* but not by bar/baz or baz.go or az or b.* + -vpath= + comma-separated list of regexppattern=N settings for file pathname-filtered + logging (without the .go suffix). E.g. foo/bar/baz.go is matched by patterns + foo/bar/baz or fo.*az or oo/ba or b.z but not by foo/bar/baz.go or fo*az + +Tidy binary - Binary sub-command tidies a specified binaryd + +Binary sub-command removes all binaries from a specified binaryd that are not +referenced by an applicationd envelope stored in the specified applicationd. + +Usage: + tidy binary [flags] <applicationd> <binaryd> + +<applicationd> is the name or endpoint of the applicationd instance sourcing the +envelopes. <binaryd> is the name or endpoint of a binaryd instance to clean up. + +Tidy application - Application sub-command tidies a specified applicationd + +Application sub-command uses the Tidy RPC to clean up outdated envelopes from +the specified appilcationd. Call this before tidying a binaryd instance for +maximum tidiness. + +Usage: + tidy application [flags] <applicationd> + +<applicationd> is the name or endpoint of the applicationd instance to tidy. + +Tidy help - Display help for commands or topics + +Help with no args displays the usage of the parent command. + +Help with args displays the usage of the specified sub-command or help topic. + +"help ..." recursively displays help for all commands and topics. + +Usage: + tidy help [flags] [command/topic ...] + +[command/topic ...] optionally identifies a specific sub-command or help topic. + +The tidy help flags are: + -style=compact + The formatting style for help output: + compact - Good for compact cmdline output. + full - Good for cmdline output, shows all global flags. + godoc - Good for godoc processing. + shortonly - Only output short description. + Override the default by setting the CMDLINE_STYLE environment variable. + -width=<terminal width> + Format output to this target width in runes, or unlimited if width < 0. + Defaults to the terminal width if available. Override the default by setting + the CMDLINE_WIDTH environment variable. +*/ +package main diff --git a/x/ref/services/binary/tidy/impl.go b/x/ref/services/binary/tidy/impl.go new file mode 100644 index 000000000..7099278a6 --- /dev/null +++ b/x/ref/services/binary/tidy/impl.go @@ -0,0 +1,225 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The following enables go generate to generate the doc.go file. +//go:generate gendoc . + +package main + +import ( + "fmt" + "sort" + "time" + + "v.io/v23" + "v.io/v23/context" + "v.io/v23/naming" + "v.io/x/lib/cmdline" + "v.io/x/lib/set" + "v.io/x/lib/vlog" + "v.io/x/ref/lib/v23cmd" + _ "v.io/x/ref/runtime/factories/generic" + "v.io/x/ref/services/internal/binarylib" + "v.io/x/ref/services/internal/profiles" + "v.io/x/ref/services/repository" +) + +var cmdBinaryTidy = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runBinaryTidy), + Name: "binary", + Short: "Binary sub-command tidies a specified binaryd", + Long: ` +Binary sub-command removes all binaries from a specified binaryd that +are not referenced by an applicationd envelope stored in the specified +applicationd. +`, + ArgsName: "<applicationd> <binaryd>", + ArgsLong: ` +<applicationd> is the name or endpoint of the applicationd instance +sourcing the envelopes. +<binaryd> is the name or endpoint of a binaryd instance to clean up. +`, +} + +// simpleGlob globs the provided endpoint as the namespace cmd does. +func mapGlob(ctx *context.T, pattern string, mapFunc func(string)) (error, []error) { + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + ns := v23.GetNamespace(ctx) + c, err := ns.Glob(ctx, pattern) + if err != nil { + vlog.Infof("ns.Glob(%q) failed: %v", pattern, err) + return err, nil + } + + errors := []*naming.GlobError{} + for res := range c { + switch v := res.(type) { + case *naming.GlobReplyEntry: + if v.Value.Name != "" { + mapFunc(v.Value.Name) + } + case *naming.GlobReplyError: + errors = append(errors, &v.Value) + } + } + + globErrors := make([]error, 0, len(errors)) + for _, err := range errors { + globErrors = append(globErrors, fmt.Errorf("Glob error: %s: %v\n", err.Name, err.Error)) + } + return nil, globErrors +} + +func logGlobErrors(env *cmdline.Env, errors []error) { + for _, err := range errors { + vlog.Errorf("Glob error: %v", err) + } +} + +// getProfileNames uses glob to extract the list of profile names +// available from the binary server specified by endpoint. +func getProfileNames(ctx *context.T, env *cmdline.Env, endpoint string) ([]string, error) { + profiles, err := profiles.GetKnownProfiles() + if err != nil { + return nil, err + } + + pnames := make([]string, 0, len(profiles)) + for _, p := range profiles { + pnames = append(pnames, p.Label) + } + return pnames, nil +} + +func getNames(ctx *context.T, env *cmdline.Env, endpoint string) ([]string, error) { + resultSet := make(map[string]struct{}) + err, errors := mapGlob(ctx, endpoint, func(s string) { + resultSet[s] = struct{}{} + }) + + if err != nil { + return nil, err + } + logGlobErrors(env, errors) + s := set.String.ToSlice(resultSet) + sort.Strings(s) + return s, nil +} + +func runBinaryTidy(ctx *context.T, env *cmdline.Env, args []string) error { + if expected, got := 2, len(args); expected != got { + return env.UsageErrorf("match: incorrect number of arguments, expected %d, got %d", expected, got) + } + + appEndpoint, binEndpoint := args[0], args[1] + + profileNames, err := getProfileNames(ctx, env, binEndpoint) + if err != nil { + return err + } + + envelopeNames, err := getNames(ctx, env, naming.Join(appEndpoint, "...")) + if err != nil { + return err + } + + // Add every path in use to a set. Deletion scope is limited to + // only the binEndpoint. + bpaths := make(map[string]struct{}) + for _, en := range envelopeNames { + // convert an envelope name into an envelope. + ac := repository.ApplicationClient(en) + + for _, p := range profileNames { + e, err := ac.Match(ctx, []string{p}) + if err != nil { + // This error case is very noisy. + vlog.VI(2).Infof("applicationd.Match(%s, %s) failed: %v\n", en, p, err) + continue + } + + root, relative := naming.SplitAddressName(e.Binary.File) + if root == binEndpoint || root == "" { + bpaths[relative] = struct{}{} + } + for _, sf := range e.Packages { + root, relative := naming.SplitAddressName(sf.File) + if root == binEndpoint || root == "" { + bpaths[relative] = struct{}{} + } + } + } + } + + binaryNames, err := getNames(ctx, env, naming.Join(binEndpoint, "...")) + if err != nil { + return err + } + + deletionCandidates := make([]int, 0, len(binaryNames)-len(envelopeNames)) + for i, bn := range binaryNames { + _, relative := naming.SplitAddressName(bn) + if _, ok := bpaths[relative]; ok { + // relative is mentioned in an envelope. + continue + } + + if _, err := binarylib.Stat(ctx, bn); err != nil { + // This name is not a binary. + continue + } + deletionCandidates = append(deletionCandidates, i) + } + + for _, i := range deletionCandidates { + b := binaryNames[i] + if err := binarylib.Delete(ctx, b); err != nil { + vlog.Errorf("Couldn't delete binary %s: %v", b, err) + } + } + + return nil +} + +var cmdApplicationTidy = &cmdline.Command{ + Runner: v23cmd.RunnerFunc(runApplicationTidy), + Name: "application", + Short: "Application sub-command tidies a specified applicationd", + Long: ` +Application sub-command uses the Tidy RPC to clean up outdated +envelopes from the specified appilcationd. Call this before tidying a +binaryd instance for maximum tidiness. +`, + ArgsName: "<applicationd>", + ArgsLong: ` +<applicationd> is the name or endpoint of the applicationd instance +to tidy. +`, +} + +func runApplicationTidy(ctx *context.T, env *cmdline.Env, args []string) error { + vlog.Infof("runApplicationTidy") + if expected, got := 1, len(args); expected != got { + return env.UsageErrorf("match: incorrect number of arguments, expected %d, got %d", expected, got) + } + ac := repository.ApplicationClient(args[0]) + return ac.TidyNow(ctx) +} + +var cmdRoot = &cmdline.Command{ + Name: "tidy", + Short: "Tidy binary repositories", + Long: ` +Tidy tidies the Vanadium repository by removing unused +envelopes and binaries. +`, + Children: []*cmdline.Command{cmdBinaryTidy, cmdApplicationTidy}, +} + +func main() { + cmdline.HideGlobalFlagsExcept() + cmdline.Main(cmdRoot) +} diff --git a/x/ref/services/binary/tidy/impl_test.go b/x/ref/services/binary/tidy/impl_test.go new file mode 100644 index 000000000..606ff20b0 --- /dev/null +++ b/x/ref/services/binary/tidy/impl_test.go @@ -0,0 +1,384 @@ +// Copyright 2015 The Vanadium 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 main + +import ( + "bytes" + "fmt" + "reflect" + "strings" + "testing" + + "v.io/v23" + "v.io/v23/services/application" + "v.io/x/lib/cmdline" + "v.io/x/ref/lib/v23cmd" + _ "v.io/x/ref/runtime/factories/generic" + "v.io/x/ref/services/binary/tidy/appd" + "v.io/x/ref/services/binary/tidy/binaryd" + "v.io/x/ref/services/internal/servicetest" + "v.io/x/ref/test" +) + +func TestApplicationTidying(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + apptape := servicetest.NewTape() + ctx, appserver, err := v23.WithNewDispatchingServer(ctx, "", appd.NewDispatcher(t, apptape)) + if err != nil { + t.Fatalf("applicationd NewDispatchingServer failed: %v", err) + } + + // Setup the command-line. + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + applicationName := appserver.Status().Endpoints[0].Name() + + apptape.SetResponses( + // TidyNow() + nil, + ) + + if err := v23cmd.ParseAndRunForTest(cmdApplicationTidy, ctx, env, []string{applicationName}); err != nil { + t.Errorf("error: %v", err) + } + + // Verify no output. + if expected, got := "", strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected output from tidy application. Got %q, expected %q", got, expected) + } + if expected, got := "", strings.TrimSpace(stderr.String()); got != expected { + t.Errorf("Unexpected error from tidy application. Got %q, expected %q", got, expected) + } + + // Verify application tape. + if got, expected := apptape.Play(), []interface{}{ + "TidyNow", + }; !reflect.DeepEqual(expected, got) { + t.Errorf("apptape invalid call sequence. Got %#v, want %#v", got, expected) + } +} + +func TestBinaryTidying(t *testing.T) { + ctx, shutdown := test.V23Init() + defer shutdown() + + binarytape := servicetest.NewTape() + ctx, binserver, err := v23.WithNewDispatchingServer(ctx, "", binaryd.NewDispatcher(t, binarytape)) + if err != nil { + t.Fatalf("binaryd NewDispatchingServer failed: %v", err) + } + + apptape := servicetest.NewTape() + ctx, appserver, err := v23.WithNewDispatchingServer(ctx, "", appd.NewDispatcher(t, apptape)) + if err != nil { + t.Fatalf("applicationd NewDispatchingServer failed: %v", err) + } + + // Setup the command-line. + var stdout, stderr bytes.Buffer + env := &cmdline.Env{Stdout: &stdout, Stderr: &stderr} + binaryName := binserver.Status().Endpoints[0].Name() + applicationName := appserver.Status().Endpoints[0].Name() + + binarytape.SetResponses( + // Glob for all binary names + binaryd.GlobResponse{[]string{ + "binaries", + "binaries/applicationd", + "binaries/applicationd/darwin-amd64", + "binaries/applicationd/darwin-amd64/app-darwin-amd-1", + "binaries/applicationd/darwin-amd64/app-darwin-amd-2", + "binaries/applicationd/linux-amd64", + "binaries/applicationd/linux-amd64/app-linux-amd-1", + "binaries/applicationd/linux-amd64/app-linux-amd-2", + "binaries/binaryd", + "binaries/binaryd/linux-amd64", + "binaries/binaryd/linux-amd64/bind-linux-amd-1", + "binaries/binaryd/linux-amd64/bind-linux-amd-2", + "binaries/binaryd/linux-amd64/bind-linux-amd-3", + "binaries/libraries", + "binaries/libraries/linux-amd64", + "binaries/libraries/linux-amd64/extra-goo-1", + }, + nil, + }, + + // Stat calls + fmt.Errorf("binaries"), + fmt.Errorf("binaries/applicationd"), + fmt.Errorf("binaries/applicationd/darwin-amd64"), + nil, // binaries/applicationd/darwin-amd64/app-darwin-amd-1 + fmt.Errorf("binaries/applicationd/linux-amd64"), + nil, // binaries/applicationd/linux-amd64/app-linux-amd-1 + fmt.Errorf("binaries/binaryd"), + fmt.Errorf("binaries/binaryd/linux-amd64"), + nil, // binaries/binaryd/linux-amd64/bind-linux-amd-1 + nil, // binaries/binaryd/linux-amd64/bind-linux-amd-2 + fmt.Errorf("binaries/libraries"), + fmt.Errorf("binaries/libraries/linux-amd64"), + + // Deletion of five binaries. + nil, + nil, + nil, + nil, + nil, + ) + + apptape.SetResponses( + // Glob for all versions of all apps + binaryd.GlobResponse{[]string{ + "applications", + "applications/applicationd", + "applications/applicationd/0", + "applications/binaryd", + "applications/binaryd/1", + }, + nil, + }, + + // applications.Match(linux-amd64) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications.Match(linux-amd64)"), + }, + // applications.Match(linux-386) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications.Match(linux-386)"), + }, + // applications.Match(linux-arm) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications.Match(linux-arm)"), + }, + // applications.Match(darwin-amd64) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications.Match(darwin-amd64)"), + }, + // applications.Match(android-arm) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications.Match(android-arm)"), + }, + // applications/applicationd.Match(linux-amd64) + appd.MatchResult{ + application.Envelope{ + Binary: application.SignedFile{ + File: "binaries/applicationd/linux-amd64/app-linux-amd-2", + }, + }, + nil, + }, + // applications/applicationd.Match(linux-386) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications/applicationd.Match(linux-386)"), + }, + // applications/applicationd.Match(linux-arm) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications/applicationd.Match(linux-arm)"), + }, + // applications/applicationd.Match(darwin-amd64) + appd.MatchResult{ + application.Envelope{ + Binary: application.SignedFile{ + File: "binaries/applicationd/darwin-amd64/app-darwin-amd-2", + }, + }, + nil, + }, + // applications/applicationd.Match(android-arm) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications/applicationd.Match(android-arm)"), + }, + // applications/applicationd/0.Match(linux-amd64) + appd.MatchResult{ + application.Envelope{ + Binary: application.SignedFile{ + File: "binaries/applicationd/linux-amd64/app-linux-amd-2", + }, + }, + nil, + }, + // applications/applicationd/0.Match(linux-386) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications/applicationd/0.Match(linux-386)"), + }, + // applications/applicationd/0.Match(linux-arm) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications/applicationd/0.Match(linux-arm)"), + }, + // applications/applicationd/0.Match(darwin-amd64) + appd.MatchResult{ + application.Envelope{ + Binary: application.SignedFile{ + File: "binaries/applicationd/darwin-amd64/app-darwin-amd-2", + }, + }, + nil, + }, + // applications/applicationd/0.Match(android-arm) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications/applicationd/0.Match(android-arm)"), + }, + // applications/binaryd.Match(linux-amd64) + appd.MatchResult{ + application.Envelope{ + Binary: application.SignedFile{ + File: "binaries/binaryd/linux-amd64/bind-linux-amd-3", + }, + Packages: application.Packages{ + "somewhere": { + File: "binaries/libraries/linux-amd64/extra-goo-1", + }, + }, + }, + nil, + }, + // applications/binaryd.Match(linux-386) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications/binaryd.Match(linux-386)"), + }, + // applications/binaryd.Match(linux-arm) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications/binaryd.Match(linux-arm)"), + }, + // applications/binaryd.Match(darwin-amd64) + appd.MatchResult{ + application.Envelope{ + Binary: application.SignedFile{ + // Deliberately doesn't exist to show that this case is correctly handled. + File: "binaries/binaryd/darwin-amd64/bind-darwin-amd-2", + }, + }, + nil, + }, + // applications/binaryd.Match(android-arm) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications/binaryd.Match(android-arm)"), + }, + // applications/binaryd/1.Match(linux-amd64) + appd.MatchResult{ + application.Envelope{ + Binary: application.SignedFile{ + File: "binaries/binaryd/linux-amd64/bind-linux-amd-3", + }, + Packages: application.Packages{ + "somewhere": { + File: "binaries/libraries/linux-amd64/extra-goo-1", + }, + }, + }, + nil, + }, + // applications/binaryd/1.Match(linux-386) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications/binaryd/1.Match(linux-386)"), + }, + // applications/binaryd/1.Match(linux-arm) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications/binaryd/1.Match(linux-arm)"), + }, + // applications/binaryd/1.Match(darwin-amd64) + appd.MatchResult{ + application.Envelope{ + Binary: application.SignedFile{ + // Deliberately doesn't exist to show that this case is correctly handled. + File: "binaries/binaryd/darwin-amd64/bind-darwin-amd-2", + }, + }, + nil, + }, + // applications/binaryd/1.Match(android-arm) + appd.MatchResult{ + application.Envelope{}, + fmt.Errorf("no applications/binaryd/1.Match(android-arm)"), + }, + ) + + if err := v23cmd.ParseAndRunForTest(cmdBinaryTidy, ctx, env, []string{applicationName, binaryName}); err != nil { + t.Errorf("error: %v", err) + } + + // Verify no output. + if expected, got := "", strings.TrimSpace(stdout.String()); got != expected { + t.Errorf("Unexpected output from tidy binary. Got %q, expected %q", got, expected) + } + if expected, got := "", strings.TrimSpace(stderr.String()); got != expected { + t.Errorf("Unexpected error from tidy binary. Got %q, expected %q", got, expected) + } + + // Verify binaryd tape. + if got, expected := binarytape.Play(), []interface{}{ + binaryd.GlobStimulus{Pattern: "..."}, + + binaryd.StatStimulus{Op: "Stat", Suffix: "binaries"}, + binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/applicationd"}, + binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/applicationd/darwin-amd64"}, + binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/applicationd/darwin-amd64/app-darwin-amd-1"}, + binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/applicationd/linux-amd64"}, + binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/applicationd/linux-amd64/app-linux-amd-1"}, + binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/binaryd"}, + binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/binaryd/linux-amd64"}, + binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/binaryd/linux-amd64/bind-linux-amd-1"}, + binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/binaryd/linux-amd64/bind-linux-amd-2"}, + binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/libraries"}, + binaryd.StatStimulus{Op: "Stat", Suffix: "binaries/libraries/linux-amd64"}, + + binaryd.DeleteStimulus{Op: "Delete", Suffix: "binaries/applicationd/darwin-amd64/app-darwin-amd-1"}, + binaryd.DeleteStimulus{Op: "Delete", Suffix: "binaries/applicationd/linux-amd64/app-linux-amd-1"}, + binaryd.DeleteStimulus{Op: "Delete", Suffix: "binaries/binaryd/linux-amd64/bind-linux-amd-1"}, + binaryd.DeleteStimulus{Op: "Delete", Suffix: "binaries/binaryd/linux-amd64/bind-linux-amd-2"}, + }; !reflect.DeepEqual(expected, got) { + t.Errorf("binarytape invalid call sequence. Got %#v, want %#v", got, expected) + } + + // Verify application tape. + if got, expected := apptape.Play(), []interface{}{ + binaryd.GlobStimulus{"..."}, + appd.MatchStimulus{Name: "Match", Suffix: "applications", Profiles: []string{"linux-amd64"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications", Profiles: []string{"linux-386"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications", Profiles: []string{"linux-arm"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications", Profiles: []string{"darwin-amd64"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications", Profiles: []string{"android-arm"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd", Profiles: []string{"linux-amd64"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd", Profiles: []string{"linux-386"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd", Profiles: []string{"linux-arm"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd", Profiles: []string{"darwin-amd64"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd", Profiles: []string{"android-arm"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd/0", Profiles: []string{"linux-amd64"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd/0", Profiles: []string{"linux-386"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd/0", Profiles: []string{"linux-arm"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd/0", Profiles: []string{"darwin-amd64"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/applicationd/0", Profiles: []string{"android-arm"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd", Profiles: []string{"linux-amd64"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd", Profiles: []string{"linux-386"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd", Profiles: []string{"linux-arm"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd", Profiles: []string{"darwin-amd64"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd", Profiles: []string{"android-arm"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd/1", Profiles: []string{"linux-amd64"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd/1", Profiles: []string{"linux-386"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd/1", Profiles: []string{"linux-arm"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd/1", Profiles: []string{"darwin-amd64"}}, + appd.MatchStimulus{Name: "Match", Suffix: "applications/binaryd/1", Profiles: []string{"android-arm"}}, + }; !reflect.DeepEqual(expected, got) { + t.Errorf("apptape invalid call sequence. Got %#v, want %#v", got, expected) + } + +} diff --git a/x/ref/services/internal/fs/only_for_test.go b/x/ref/services/internal/fs/only_for_test.go new file mode 100644 index 000000000..eeb2cb311 --- /dev/null +++ b/x/ref/services/internal/fs/only_for_test.go @@ -0,0 +1,47 @@ +// Copyright 2015 The Vanadium 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 fs + +import ( + "v.io/v23/naming" + "v.io/v23/security" + "v.io/v23/services/application" +) + +// TP is a convenience function. It prepends the transactionNamePrefix +// to the given path. +func TP(path string) string { + return naming.Join(transactionNamePrefix, path) +} + +func (ms *Memstore) PersistedFile() string { + return ms.persistedFile +} + +func translateToGobEncodeable(in interface{}) interface{} { + env, ok := in.(application.Envelope) + if !ok { + return in + } + return applicationEnvelope{ + Title: env.Title, + Args: env.Args, + Binary: env.Binary, + Publisher: security.MarshalBlessings(env.Publisher), + Env: env.Env, + Packages: env.Packages, + } +} + +func (ms *Memstore) GetGOBConvertedMemstore() map[string]interface{} { + convertedMap := make(map[string]interface{}) + for k, v := range ms.data { + switch tv := v.(type) { + case application.Envelope: + convertedMap[k] = translateToGobEncodeable(tv) + } + } + return convertedMap +} diff --git a/x/ref/services/internal/fs/simplestore.go b/x/ref/services/internal/fs/simplestore.go new file mode 100644 index 000000000..44c37e782 --- /dev/null +++ b/x/ref/services/internal/fs/simplestore.go @@ -0,0 +1,573 @@ +// Copyright 2015 The Vanadium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Implements a map-based store substitute that implements the legacy store API. +package fs + +import ( + "encoding/gob" + "fmt" + "io" + "io/ioutil" + "os" + "reflect" + "sort" + "strings" + "sync" + "time" + + "v.io/v23/security" + "v.io/v23/security/access" + "v.io/v23/services/application" + "v.io/v23/verror" + "v.io/v23/vom" + "v.io/x/lib/set" + "v.io/x/ref/services/profile" +) + +// TODO(rjkroege@google.com) Switch Memstore to the mid-August 2014 +// style store API. + +const pkgPath = "v.io/x/ref/services/internal/fs" + +// Errors +var ( + ErrNoRecursiveCreateTransaction = verror.Register(pkgPath+".ErrNoRecursiveCreateTransaction", verror.NoRetry, "{1:}{2:} recursive CreateTransaction() not permitted{:_}") + ErrDoubleCommit = verror.Register(pkgPath+".ErrDoubleCommit", verror.NoRetry, "{1:}{2:} illegal attempt to commit previously committed or abandonned transaction{:_}") + ErrAbortWithoutTransaction = verror.Register(pkgPath+".ErrAbortWithoutTransaction", verror.NoRetry, "{1:}{2:} illegal attempt to abort non-existent transaction{:_}") + ErrWithoutTransaction = verror.Register(pkgPath+".ErrRemoveWithoutTransaction", verror.NoRetry, "{1:}{2:} call without a transaction{:_}") + ErrNotInMemStore = verror.Register(pkgPath+".ErrNotInMemStore", verror.NoRetry, "{1:}{2:} not in Memstore{:_}") + ErrUnsupportedType = verror.Register(pkgPath+".ErrUnsupportedType", verror.NoRetry, "{1:}{2:} attempted Put to Memstore of unsupported type{:_}") + ErrChildrenWithoutLock = verror.Register(pkgPath+".ErrChildrenWithoutLock", verror.NoRetry, "{1:}{2:} Children() without a lock{:_}") + + errTempFileFailed = verror.Register(pkgPath+".errTempFileFailed", verror.NoRetry, "{1:}{2:} TempFile({3}, {4}) failed{:_}") + errCantCreate = verror.Register(pkgPath+".errCantCreate", verror.NoRetry, "{1:}{2:} File ({3}) could not be created ({4}){:_}") + errCantOpen = verror.Register(pkgPath+".errCantOpen", verror.NoRetry, "{1:}{2:} File ({3}) could not be opened ({4}){:_}") + errDecodeFailedBadData = verror.Register(pkgPath+".errDecodeFailedBadData", verror.NoRetry, "{1:}{2:} Decode() failed: data format mismatch or backing file truncated{:_}") + errCreateFailed = verror.Register(pkgPath+".errCreateFailed", verror.NoRetry, "{1:}{2:} Create({3}) failed{:_}") + errEncodeFailed = verror.Register(pkgPath+".errEncodeFailed", verror.NoRetry, "{1:}{2:} Encode() failed{:_}") + errFileSystemError = verror.Register(pkgPath+".errFileSystemError", verror.NoRetry, "{1:}{2:} File system operation failed{:_}") + errFormatUpgradeError = verror.Register(pkgPath+".errFormatUpgradeError", verror.NoRetry, "{1:}{2:} File format upgrading failed{:_}") +) + +// Memstore contains the state of the memstore. It supports a single +// transaction at a time. The current state of a Memstore under a +// transactional name binding is the contents of puts then the contents +// of (data - removes). puts and removes will be empty at the beginning +// of a transaction and after an Unlock operation. +type Memstore struct { + sync.Mutex + persistedFile string + haveTransactionNameBinding bool + locked bool + data map[string]interface{} + puts map[string]interface{} + removes map[string]struct{} +} + +const ( + startingMemstoreSize = 10 + transactionNamePrefix = "memstore-transaction" + + // A memstore is a serialized Go map. A GOB-encoded map using Go 1.4 + // cannot begin with this byte. Consequently, simplestore writes this + // magic byte to the start of a vom file. Its absence at the + // beginning of the file indicates that the memstore file is using + // the GOB legacy encoding. + vomGobMagicByte = 0xF0 +) + +var keyExists = struct{}{} + +// TODO(rjkroege): Simplestore used GOB for its persistence +// layer. However, now, it uses VOM to store persisted values and +// automatically converts GOB format files to VOM-compatible on load. +// At some point in the future, it may be possible to simplify the +// implementation by removing support for loading files in the +// now legacy GOB format. +type applicationEnvelope struct { + Title string + Args []string + Binary application.SignedFile + Publisher security.WireBlessings + Env []string + Packages application.Packages + Restarts int32 + RestartTimeWindow time.Duration +} + +// This function is needed only to support existing serialized data and +// can be removed in a future release. +func translateFromGobEncodeable(in interface{}) (interface{}, error) { + env, ok := in.(applicationEnvelope) + if !ok { + return in, nil + } + // Have to roundtrip through vom to convert from WireBlessings to Blessings. + // This may seem silly, but this whole translation business is silly too :) + // and will go away once we switch this package to using 'vom' instead of 'gob'. + // So for now, live with the funkiness. + bytes, err := vom.Encode(env.Publisher) + if err != nil { + return nil, err + } + var publisher security.Blessings + if err := vom.Decode(bytes, &publisher); err != nil { + return nil, err + } + return application.Envelope{ + Title: env.Title, + Args: env.Args, + Binary: env.Binary, + Publisher: publisher, + Env: env.Env, + Packages: env.Packages, + Restarts: env.Restarts, + RestartTimeWindow: env.RestartTimeWindow, + }, nil +} + +// The implementation of set requires gob instead of json. +func init() { + gob.Register(profile.Specification{}) + gob.Register(applicationEnvelope{}) + gob.Register(access.Permissions{}) + // Ensure that no fields have been added to application.Envelope, + // because if so, then applicationEnvelope defined in this package + // needs to change + if n := reflect.TypeOf(application.Envelope{}).NumField(); n != 8 { + panic(fmt.Sprintf("It appears that fields have been added to or removed from application.Envelope before the hack in this file around gob-encodeability was removed. Please also update applicationEnvelope, translateToGobEncodeable and translateToGobDecodeable in this file")) + } +} + +// isVOM returns true if the file is a VOM-format file. +func isVOM(file io.ReadSeeker) (bool, error) { + oneByte := make([]byte, 1) + c, err := file.Read(oneByte) + for c == 0 && err == nil { + c, err = file.Read(oneByte) + } + + if c > 0 { + if oneByte[0] == vomGobMagicByte { + return true, nil + } else { + if _, err := file.Seek(0, 0); err != nil { + return false, verror.New(errFileSystemError, nil, err) + } + return false, nil + } + } + return false, err +} + +func nonEmptyExists(fileName string) (bool, error) { + fi, serr := os.Stat(fileName) + if os.IsNotExist(serr) { + return false, nil + } else if serr != nil { + return false, serr + } + if fi.Size() > 0 { + return true, nil + } + return false, nil +} + +func convertFileToVomIfNecessary(fileName string) error { + // Open the legacy file. + file, err := os.Open(fileName) + if err != nil { + return verror.New(errCantOpen, nil, fileName, err) + } + defer file.Close() + + // VOM files don't need conversion. + if is, err := isVOM(file); is || err != nil { + return err + } + + // Decode the legacy GOB file + decoder := gob.NewDecoder(file) + data := make(map[string]interface{}, startingMemstoreSize) + if err := decoder.Decode(&data); err != nil { + return verror.New(errDecodeFailedBadData, nil, err) + } + + // Update GOB file to VOM format in memory. + for k, v := range data { + tv, err := translateFromGobEncodeable(v) + if err != nil { + return verror.New(errFormatUpgradeError, nil, err) + } + data[k] = tv + } + + ms := &Memstore{ + data: data, + persistedFile: fileName, + } + if err := ms.persist(); err != nil { + return err + } + return nil +} + +// NewMemstore persists the Memstore to os.TempDir() if no file is +// configured. +func NewMemstore(configuredPersistentFile string) (*Memstore, error) { + data := make(map[string]interface{}, startingMemstoreSize) + if configuredPersistentFile == "" { + f, err := ioutil.TempFile(os.TempDir(), "memstore-vom") + if err != nil { + return nil, verror.New(errTempFileFailed, nil, os.TempDir(), "memstore-vom", err) + } + f.Close() + configuredPersistentFile = f.Name() + } + + // Empty or non-existent files. + nee, err := nonEmptyExists(configuredPersistentFile) + if err != nil { + return nil, verror.New(errCantCreate, nil, configuredPersistentFile, err) + } + if !nee { + // Ensure that we can create a file. + file, cerr := os.Create(configuredPersistentFile) + if cerr != nil { + return nil, verror.New(errCantCreate, nil, configuredPersistentFile, err, cerr) + } + file.Close() + return &Memstore{ + data: data, + persistedFile: configuredPersistentFile, + }, nil + } + + // Convert a non-empty GOB file into VOM format. + if err := convertFileToVomIfNecessary(configuredPersistentFile); err != nil { + return nil, err + } + + file, err := os.Open(configuredPersistentFile) + if err != nil { + return nil, verror.New(errCantOpen, nil, configuredPersistentFile, err) + } + // Skip past the magic byte that identifies this as a VOM format file. + if _, err := file.Seek(1, 0); err != nil { + return nil, verror.New(errFileSystemError, nil, err) + } + + decoder := vom.NewDecoder(file) + if err := decoder.Decode(&data); err != nil { + return nil, verror.New(errDecodeFailedBadData, nil, err) + } + + return &Memstore{ + data: data, + persistedFile: configuredPersistentFile, + }, nil +} + +type MemstoreObject interface { + Remove(_ interface{}) error + Exists(_ interface{}) (bool, error) +} + +type boundObject struct { + path string + ms *Memstore + Value interface{} +} + +// BindObject sets the path string for subsequent operations. +func (ms *Memstore) BindObject(path string) *boundObject { + pathParts := strings.SplitN(path, "/", 2) + if pathParts[0] == transactionNamePrefix { + ms.haveTransactionNameBinding = true + } else { + ms.haveTransactionNameBinding = false + } + return &boundObject{path: pathParts[1], ms: ms} +} + +func (ms *Memstore) removeChildren(path string) bool { + deleted := false + for k, _ := range ms.data { + if strings.HasPrefix(k, path) { + deleted = true + ms.removes[k] = keyExists + } + } + for k, _ := range ms.puts { + if strings.HasPrefix(k, path) { + deleted = true + delete(ms.puts, k) + } + } + return deleted +} + +type Transaction interface { + CreateTransaction(_ interface{}) (string, error) + Commit(_ interface{}) error +} + +// BindTransactionRoot on a Memstore always operates over the +// entire Memstore. As a result, the root parameter is ignored. +func (ms *Memstore) BindTransactionRoot(_ string) Transaction { + return ms +} + +// BindTransaction on a Memstore can only use the single Memstore +// transaction. +func (ms *Memstore) BindTransaction(_ string) Transaction { + return ms +} + +func (ms *Memstore) newTransactionState() { + ms.puts = make(map[string]interface{}, startingMemstoreSize) + ms.removes = make(map[string]struct{}, startingMemstoreSize) +} + +func (ms *Memstore) clearTransactionState() { + ms.puts = nil + ms.removes = nil +} + +// Unlock abandons an in-progress transaction before releasing the lock. +func (ms *Memstore) Unlock() { + ms.locked = false + ms.clearTransactionState() + ms.Mutex.Unlock() +} + +// Lock acquires a lock and caches the state of the lock. +func (ms *Memstore) Lock() { + ms.Mutex.Lock() + ms.clearTransactionState() + ms.locked = true +} + +// CreateTransaction requires the caller to acquire a lock on the Memstore. +func (ms *Memstore) CreateTransaction(_ interface{}) (string, error) { + if ms.puts != nil || ms.removes != nil { + return "", verror.New(ErrNoRecursiveCreateTransaction, nil) + } + ms.newTransactionState() + return transactionNamePrefix, nil +} + +// Commit updates the store and persists the result. +func (ms *Memstore) Commit(_ interface{}) error { + if !ms.locked || ms.puts == nil || ms.removes == nil { + return verror.New(ErrDoubleCommit, nil) + } + for k, v := range ms.puts { + ms.data[k] = v + } + for k, _ := range ms.removes { + delete(ms.data, k) + } + return ms.persist() +} + +func (ms *Memstore) Abort(_ interface{}) error { + if !ms.locked { + return verror.New(ErrAbortWithoutTransaction, nil) + } + return nil +} + +func (o *boundObject) Remove(_ interface{}) error { + if !o.ms.locked { + return verror.New(ErrWithoutTransaction, nil, "Remove()") + } + + if _, pendingRemoval := o.ms.removes[o.path]; pendingRemoval { + return verror.New(ErrNotInMemStore, nil, o.path) + } + + _, found := o.ms.data[o.path] + if !found && !o.ms.removeChildren(o.path) { + return verror.New(ErrNotInMemStore, nil, o.path) + } + delete(o.ms.puts, o.path) + o.ms.removes[o.path] = keyExists + return nil +} + +// transactionExists implements Exists() for bound names that have the +// transaction prefix. +func (o *boundObject) transactionExists() bool { + // Determine if the bound name point to a real object. + _, inBase := o.ms.data[o.path] + _, inPuts := o.ms.puts[o.path] + _, inRemoves := o.ms.removes[o.path] + + // not yet committed. + if inPuts || (inBase && !inRemoves) { + return true + } + + // The bound names might be a prefix of the path for a real object. For + // example, BindObject("/test/a"). Put(o) creates a real object o at path + /// test/a so the code above will cause BindObject("/test/a").Exists() to + // return true. Testing this is however not sufficient because + // BindObject(any prefix of on object path).Exists() needs to also be + // true. For example, here BindObject("/test").Exists() is true. + // + // Consequently, transactionExists scans all object names in the Memstore + // to determine if any of their names have the bound name as a prefix. + // Puts take precedence over removes so we scan it first. + + for k, _ := range o.ms.puts { + if strings.HasPrefix(k, o.path) { + return true + } + } + + // Then we scan data for matches and verify that at least one of the + // object names with the bound prefix have not been removed. + + for k, _ := range o.ms.data { + if _, inRemoves := o.ms.removes[k]; strings.HasPrefix(k, o.path) && !inRemoves { + return true + } + } + return false +} + +func (o *boundObject) Exists(_ interface{}) (bool, error) { + if o.ms.haveTransactionNameBinding { + return o.transactionExists(), nil + } else { + _, inBase := o.ms.data[o.path] + if inBase { + return true, nil + } + for k, _ := range o.ms.data { + if strings.HasPrefix(k, o.path) { + return true, nil + } + } + } + return false, nil +} + +// transactionBoundGet implements Get while the bound name has the +// transaction prefix. +func (o *boundObject) transactionBoundGet() (*boundObject, error) { + bv, inBase := o.ms.data[o.path] + _, inRemoves := o.ms.removes[o.path] + pv, inPuts := o.ms.puts[o.path] + + found := inPuts || (inBase && !inRemoves) + if !found { + return nil, verror.New(ErrNotInMemStore, nil, o.path) + } + + if inPuts { + o.Value = pv + } else { + o.Value = bv + } + var err error + if o.Value, err = translateFromGobEncodeable(o.Value); err != nil { + return nil, err + } + return o, nil +} + +func (o *boundObject) bareGet() (*boundObject, error) { + bv, inBase := o.ms.data[o.path] + + if !inBase { + return nil, verror.New(ErrNotInMemStore, nil, o.path) + } + o.Value = bv + return o, nil +} + +func (o *boundObject) Get(_ interface{}) (*boundObject, error) { + if o.ms.haveTransactionNameBinding { + return o.transactionBoundGet() + } else { + return o.bareGet() + } +} + +func (o *boundObject) Put(_ interface{}, envelope interface{}) (*boundObject, error) { + if !o.ms.locked { + return nil, verror.New(ErrWithoutTransaction, nil, "Put()") + } + switch v := envelope.(type) { + case application.Envelope, profile.Specification, access.Permissions: + o.ms.puts[o.path] = v + delete(o.ms.removes, o.path) + o.Value = o.path + return o, nil + default: + return o, verror.New(ErrUnsupportedType, nil) + } +} + +func (o *boundObject) Children() ([]string, error) { + if !o.ms.locked { + return nil, verror.New(ErrChildrenWithoutLock, nil) + } + found := false + childrenSet := make(map[string]struct{}) + for k, _ := range o.ms.data { + if strings.HasPrefix(k, o.path) || o.path == "" { + name := strings.TrimPrefix(k, o.path) + // Found the object itself. + if len(name) == 0 { + found = true + continue + } + // This was only a prefix match, not what we're looking for. + if name[0] != '/' && o.path != "" { + continue + } + found = true + name = strings.TrimLeft(name, "/") + if idx := strings.Index(name, "/"); idx != -1 { + name = name[:idx] + } + childrenSet[name] = keyExists + } + } + if !found { + return nil, verror.New(verror.ErrNoExist, nil, o.path) + } + children := set.String.ToSlice(childrenSet) + sort.Strings(children) + return children, nil +} + +// persist() writes the state of the Memstore to persistent storage. +func (ms *Memstore) persist() error { + file, err := ioutil.TempFile(os.TempDir(), "memstore-persisting") + if err != nil { + return verror.New(errTempFileFailed, nil, os.TempDir(), "memstore-persisting", err) + } + defer file.Close() + defer os.Remove(file.Name()) + + // Mark this VOM file with the VOM file format magic byte. + if _, err := file.Write([]byte{byte(vomGobMagicByte)}); err != nil { + return err + } + enc := vom.NewEncoder(file) + if err := enc.Encode(ms.data); err != nil { + return verror.New(errEncodeFailed, nil, err) + } + ms.clearTransactionState() + + if err := os.Rename(file.Name(), ms.persistedFile); err != nil { + return verror.New(errEncodeFailed, nil, err) + } + return nil +} diff --git a/x/ref/services/internal/fs/simplestore_test.go b/x/ref/services/internal/fs/simplestore_test.go new file mode 100644 index 000000000..969905fe6 --- /dev/null +++ b/x/ref/services/internal/fs/simplestore_test.go @@ -0,0 +1,595 @@ +// Copyright 2015 The Vanadium 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 fs_test + +import ( + "encoding/gob" + "fmt" + "io/ioutil" + "os" + "reflect" + "testing" + + "v.io/v23/naming" + "v.io/v23/services/application" + "v.io/v23/verror" + "v.io/x/ref/services/internal/fs" + _ "v.io/x/ref/services/profile" +) + +func tempFile(t *testing.T) string { + tmpfile, err := ioutil.TempFile("", "simplestore-test-") + if err != nil { + t.Fatalf("ioutil.TempFile() failed: %v", err) + } + defer tmpfile.Close() + return tmpfile.Name() +} + +func TestNewMemstore(t *testing.T) { + memstore, err := fs.NewMemstore("") + + if err != nil { + t.Fatalf("fs.NewMemstore() failed: %v", err) + } + + if _, err = os.Stat(memstore.PersistedFile()); err != nil { + t.Fatalf("Stat(%v) failed: %v", memstore.PersistedFile(), err) + } + os.Remove(memstore.PersistedFile()) +} + +func TestNewNamedMemstore(t *testing.T) { + path := tempFile(t) + defer os.Remove(path) + memstore, err := fs.NewMemstore(path) + if err != nil { + t.Fatalf("fs.NewMemstore() failed: %v", err) + } + + if _, err = os.Stat(memstore.PersistedFile()); err != nil { + t.Fatalf("Stat(%v) failed: %v", path, err) + } +} + +// Verify that all of the listed paths Exists(). +// Caller is responsible for setting up any transaction state necessary. +func allPathsExist(ts *fs.Memstore, paths []string) error { + for _, p := range paths { + exists, err := ts.BindObject(p).Exists(nil) + if err != nil { + return fmt.Errorf("Exists(%s) expected to succeed but failed: %v", p, err) + } + if !exists { + return fmt.Errorf("Exists(%s) expected to be true but is false", p) + } + } + return nil +} + +// Verify that all of the listed paths !Exists(). +// Caller is responsible for setting up any transaction state necessary. +func allPathsDontExist(ts *fs.Memstore, paths []string) error { + for _, p := range paths { + exists, err := ts.BindObject(p).Exists(nil) + if err != nil { + return fmt.Errorf("Exists(%s) expected to succeed but failed: %v", p, err) + } + if exists { + return fmt.Errorf("Exists(%s) expected to be false but is true", p) + } + } + return nil +} + +type PathValue struct { + Path string + Expected interface{} +} + +// getEquals tests that every provided path is equal to the specified value. +func allPathsEqual(ts *fs.Memstore, pvs []PathValue) error { + for _, p := range pvs { + v, err := ts.BindObject(p.Path).Get(nil) + if err != nil { + return fmt.Errorf("Get(%s) expected to succeed but failed: %v", p, err) + } + if !reflect.DeepEqual(p.Expected, v.Value) { + return fmt.Errorf("Unexpected non-equality for %s: got %v, expected %v", p.Path, v.Value, p.Expected) + } + } + return nil +} + +func TestSerializeDeserialize(t *testing.T) { + path := tempFile(t) + defer os.Remove(path) + memstoreOriginal, err := fs.NewMemstore(path) + if err != nil { + t.Fatalf("fs.NewMemstore() failed: %v", err) + } + + // Create example data. + envelope := application.Envelope{ + Args: []string{"--help"}, + Env: []string{"DEBUG=1"}, + Binary: application.SignedFile{File: "/v23/name/of/binary"}, + } + secondEnvelope := application.Envelope{ + Args: []string{"--save"}, + Env: []string{"VEYRON=42"}, + Binary: application.SignedFile{File: "/v23/name/of/binary/is/memstored"}, + } + + // TRANSACTION BEGIN + // Insert a value into the fs.Memstore at /test/a + memstoreOriginal.Lock() + tname, err := memstoreOriginal.BindTransactionRoot("ignored").CreateTransaction(nil) + if err != nil { + t.Fatalf("CreateTransaction() failed: %v", err) + } + if _, err := memstoreOriginal.BindObject(fs.TP("/test/a")).Put(nil, envelope); err != nil { + t.Fatalf("Put() failed: %v", err) + } + + if err := allPathsExist(memstoreOriginal, []string{fs.TP("/test/a"), fs.TP("/test")}); err != nil { + t.Fatalf("%v", err) + } + if err := allPathsEqual(memstoreOriginal, []PathValue{{fs.TP("/test/a"), envelope}}); err != nil { + t.Fatalf("%v", err) + } + + if err := memstoreOriginal.BindTransaction(tname).Commit(nil); err != nil { + t.Fatalf("Commit() failed: %v", err) + } + memstoreOriginal.Unlock() + // TRANSACTION END + + // Validate persisted state. + if err := allPathsExist(memstoreOriginal, []string{"/test/a", "/test"}); err != nil { + t.Fatalf("%v", err) + } + if err := allPathsEqual(memstoreOriginal, []PathValue{{"/test/a", envelope}}); err != nil { + t.Fatalf("%v", err) + } + + // TRANSACTION BEGIN Write a value to /test/b as well. + memstoreOriginal.Lock() + tname, err = memstoreOriginal.BindTransactionRoot("also ignored").CreateTransaction(nil) + bindingTnameTestB := memstoreOriginal.BindObject(fs.TP("/test/b")) + if _, err := bindingTnameTestB.Put(nil, envelope); err != nil { + t.Fatalf("Put() failed: %v", err) + } + + // Validate persisted state during transaction + if err := allPathsExist(memstoreOriginal, []string{"/test/a", "/test"}); err != nil { + t.Fatalf("%v", err) + } + if err := allPathsEqual(memstoreOriginal, []PathValue{{"/test/a", envelope}}); err != nil { + t.Fatalf("%v", err) + } + // Validate pending state during transaction + if err := allPathsExist(memstoreOriginal, []string{fs.TP("/test/a"), fs.TP("/test"), fs.TP("/test/b")}); err != nil { + t.Fatalf("%v", err) + } + if err := allPathsEqual(memstoreOriginal, []PathValue{ + {fs.TP("/test/a"), envelope}, + {fs.TP("/test/b"), envelope}}); err != nil { + t.Fatalf("%v", err) + } + + // Commit the <tname>/test/b to /test/b + if err := memstoreOriginal.Commit(nil); err != nil { + t.Fatalf("Commit() failed: %v", err) + } + memstoreOriginal.Unlock() + // TODO(rjkroege): Consider ensuring that Get() on <tname>/test/b should now fail. + // TRANSACTION END + + // Validate persisted state after transaction + if err := allPathsExist(memstoreOriginal, []string{"/test/a", "/test", "/test/b"}); err != nil { + t.Fatalf("%v", err) + } + if err := allPathsEqual(memstoreOriginal, []PathValue{ + {"/test/a", envelope}, + {"/test/b", envelope}}); err != nil { + t.Fatalf("%v", err) + } + + // TRANSACTION BEGIN (to be abandonned) + memstoreOriginal.Lock() + tname, err = memstoreOriginal.BindTransactionRoot("").CreateTransaction(nil) + + // Exists is true before doing anything. + if err := allPathsExist(memstoreOriginal, []string{fs.TP("/test")}); err != nil { + t.Fatalf("%v", err) + } + + if _, err := memstoreOriginal.BindObject(fs.TP("/test/b")).Put(nil, secondEnvelope); err != nil { + t.Fatalf("Put() failed: %v", err) + } + + // Validate persisted state during transaction + if err := allPathsExist(memstoreOriginal, []string{ + "/test/a", + "/test/b", + "/test", + fs.TP("/test"), + fs.TP("/test/a"), + fs.TP("/test/b"), + }); err != nil { + t.Fatalf("%v", err) + } + if err := allPathsEqual(memstoreOriginal, []PathValue{ + {"/test/a", envelope}, + {"/test/b", envelope}, + {fs.TP("/test/b"), secondEnvelope}, + {fs.TP("/test/a"), envelope}, + }); err != nil { + t.Fatalf("%v", err) + } + + // Pending Remove() of /test + if err := memstoreOriginal.BindObject(fs.TP("/test")).Remove(nil); err != nil { + t.Fatalf("Remove() failed: %v", err) + } + + // Verify that all paths are successfully removed from the in-progress transaction. + if err := allPathsDontExist(memstoreOriginal, []string{fs.TP("/test/a"), fs.TP("/test"), fs.TP("/test/b")}); err != nil { + t.Fatalf("%v", err) + } + // But all paths remain in the persisted version. + if err := allPathsExist(memstoreOriginal, []string{"/test/a", "/test", "/test/b"}); err != nil { + t.Fatalf("%v", err) + } + if err := allPathsEqual(memstoreOriginal, []PathValue{ + {"/test/a", envelope}, + {"/test/b", envelope}, + }); err != nil { + t.Fatalf("%v", err) + } + + // At which point, Get() on the transaction won't find anything. + if _, err := memstoreOriginal.BindObject(fs.TP("/test/a")).Get(nil); verror.ErrorID(err) != fs.ErrNotInMemStore.ID { + t.Fatalf("Get() should have failed: got %v, expected %v", err, verror.New(fs.ErrNotInMemStore, nil, tname+"/test/a")) + } + + // Attempting to Remove() it over again will fail. + if err := memstoreOriginal.BindObject(fs.TP("/test/a")).Remove(nil); verror.ErrorID(err) != fs.ErrNotInMemStore.ID { + t.Fatalf("Remove() should have failed: got %v, expected %v", err, verror.New(fs.ErrNotInMemStore, nil, tname+"/test/a")) + } + + // Attempting to Remove() a non-existing path will fail. + if err := memstoreOriginal.BindObject(fs.TP("/foo")).Remove(nil); verror.ErrorID(err) != fs.ErrNotInMemStore.ID { + t.Fatalf("Remove() should have failed: got %v, expected %v", err, verror.New(fs.ErrNotInMemStore, nil, tname+"/foo")) + } + + // Exists() a non-existing path will fail. + if present, _ := memstoreOriginal.BindObject(fs.TP("/foo")).Exists(nil); present { + t.Fatalf("Exists() should have failed for non-existing path %s", tname+"/foo") + } + + // Abort the transaction without committing it. + memstoreOriginal.Abort(nil) + memstoreOriginal.Unlock() + // TRANSACTION END (ABORTED) + + // Validate that persisted state after abandonned transaction has not changed. + if err := allPathsExist(memstoreOriginal, []string{"/test/a", "/test", "/test/b"}); err != nil { + t.Fatalf("%v", err) + } + if err := allPathsEqual(memstoreOriginal, []PathValue{ + {"/test/a", envelope}, + {"/test/b", envelope}}); err != nil { + t.Fatalf("%v", err) + } + + // Validate that Get will fail on a non-existent path. + if _, err := memstoreOriginal.BindObject("/test/c").Get(nil); verror.ErrorID(err) != fs.ErrNotInMemStore.ID { + t.Fatalf("Get() should have failed: got %v, expected %v", err, verror.New(fs.ErrNotInMemStore, nil, tname+"/test/c")) + } + + // Verify that the previous Commit() operations have persisted to + // disk by creating a new Memstore from the contents on disk. + memstoreCopy, err := fs.NewMemstore(path) + if err != nil { + t.Fatalf("fs.NewMemstore() failed: %v", err) + } + // Verify that memstoreCopy is an exact copy of memstoreOriginal. + if err := allPathsExist(memstoreCopy, []string{"/test/a", "/test", "/test/b"}); err != nil { + t.Fatalf("%v", err) + } + if err := allPathsEqual(memstoreCopy, []PathValue{ + {"/test/a", envelope}, + {"/test/b", envelope}}); err != nil { + t.Fatalf("%v", err) + } + + // TRANSACTION BEGIN + memstoreCopy.Lock() + tname, err = memstoreCopy.BindTransactionRoot("also ignored").CreateTransaction(nil) + + // Add a pending object c to test that pending objects are deleted. + if _, err := memstoreCopy.BindObject(fs.TP("/test/c")).Put(nil, secondEnvelope); err != nil { + t.Fatalf("Put() failed: %v", err) + } + if err := allPathsExist(memstoreCopy, []string{ + fs.TP("/test/a"), + "/test/a", + fs.TP("/test"), + "/test", + fs.TP("/test/b"), + "/test/b", + fs.TP("/test/c"), + }); err != nil { + t.Fatalf("%v", err) + } + if err := allPathsEqual(memstoreCopy, []PathValue{ + {fs.TP("/test/a"), envelope}, + {fs.TP("/test/b"), envelope}, + {fs.TP("/test/c"), secondEnvelope}, + {"/test/a", envelope}, + {"/test/b", envelope}, + }); err != nil { + t.Fatalf("%v", err) + } + + // Remove /test/a /test/b /test/c /test + if err := memstoreCopy.BindObject(fs.TP("/test")).Remove(nil); err != nil { + t.Fatalf("Remove() failed: %v", err) + } + // Verify that all paths are successfully removed from the in-progress transaction. + if err := allPathsDontExist(memstoreCopy, []string{ + fs.TP("/test/a"), + fs.TP("/test"), + fs.TP("/test/b"), + fs.TP("/test/c"), + }); err != nil { + t.Fatalf("%v", err) + } + if err := allPathsExist(memstoreCopy, []string{ + "/test/a", + "/test", + "/test/b", + }); err != nil { + t.Fatalf("%v", err) + } + // Commit the change. + if err = memstoreCopy.Commit(nil); err != nil { + t.Fatalf("Commit() failed: %v", err) + } + memstoreCopy.Unlock() + // TRANSACTION END + + // Create a new Memstore from file to see if Remove operates are + // persisted. + memstoreRemovedCopy, err := fs.NewMemstore(path) + if err != nil { + t.Fatalf("fs.NewMemstore() failed for removed copy: %v", err) + } + if err := allPathsDontExist(memstoreRemovedCopy, []string{ + "/test/a", + "/test", + "/test/b", + "/test/c", + }); err != nil { + t.Fatalf("%v", err) + } +} + +func TestOperationsNeedValidBinding(t *testing.T) { + path := tempFile(t) + defer os.Remove(path) + memstoreOriginal, err := fs.NewMemstore(path) + if err != nil { + t.Fatalf("fs.NewMemstore() failed: %v", err) + } + + // Create example data. + envelope := application.Envelope{ + Args: []string{"--help"}, + Env: []string{"DEBUG=1"}, + Binary: application.SignedFile{File: "/v23/name/of/binary"}, + } + + // TRANSACTION BEGIN + // Attempt inserting a value at /test/a. + memstoreOriginal.Lock() + tname, err := memstoreOriginal.BindTransactionRoot("").CreateTransaction(nil) + if err != nil { + t.Fatalf("CreateTransaction() failed: %v", err) + } + + if err := memstoreOriginal.BindTransaction(tname).Commit(nil); err != nil { + t.Fatalf("Commit() failed: %v", err) + } + memstoreOriginal.Unlock() + // TRANSACTION END + + // Put outside ot a transaction should fail. + bindingTnameTestA := memstoreOriginal.BindObject(naming.Join("fooey", "/test/a")) + if _, err := bindingTnameTestA.Put(nil, envelope); verror.ErrorID(err) != fs.ErrWithoutTransaction.ID { + t.Fatalf("Put() failed: got %v, expected %v", err, verror.New(fs.ErrWithoutTransaction, nil, "Put()")) + } + + // Remove outside of a transaction should fail + if err := bindingTnameTestA.Remove(nil); verror.ErrorID(err) != fs.ErrWithoutTransaction.ID { + t.Fatalf("Put() failed: got %v, expected %v", err, verror.New(fs.ErrWithoutTransaction, nil, "Remove()")) + } + + // Commit outside of a transaction should fail + if err := memstoreOriginal.BindTransaction(tname).Commit(nil); verror.ErrorID(err) != fs.ErrDoubleCommit.ID { + t.Fatalf("Commit() failed: got %v, expected %v", err, verror.New(fs.ErrDoubleCommit, nil)) + } + + // Attempt inserting a value at /test/b + memstoreOriginal.Lock() + tname, err = memstoreOriginal.BindTransactionRoot("").CreateTransaction(nil) + if err != nil { + t.Fatalf("CreateTransaction() failed: %v", err) + } + + bindingTnameTestB := memstoreOriginal.BindObject(fs.TP("/test/b")) + if _, err := bindingTnameTestB.Put(nil, envelope); err != nil { + t.Fatalf("Put() failed: %v", err) + } + // Abandon transaction. + memstoreOriginal.Unlock() + + // Remove should definitely fail on an abndonned transaction. + if err := bindingTnameTestB.Remove(nil); verror.ErrorID(err) != fs.ErrWithoutTransaction.ID { + t.Fatalf("Remove() failed: got %v, expected %v", err, verror.New(fs.ErrWithoutTransaction, nil, "Remove()")) + } +} + +func TestOpenEmptyMemstore(t *testing.T) { + path := tempFile(t) + defer os.Remove(path) + + // Create a brand new memstore persisted to namedms. This will + // have the side-effect of creating an empty backing file. + if _, err := fs.NewMemstore(path); err != nil { + t.Fatalf("fs.NewMemstore() failed: %v", err) + } + + // Create another memstore that will attempt to deserialize the empty + // backing file. + if _, err := fs.NewMemstore(path); err != nil { + t.Fatalf("fs.NewMemstore() failed: %v", err) + } +} + +func TestChildren(t *testing.T) { + memstore, err := fs.NewMemstore("") + if err != nil { + t.Fatalf("fs.NewMemstore() failed: %v", err) + } + defer os.Remove(memstore.PersistedFile()) + + // Create example data. + envelope := application.Envelope{ + Args: []string{"--help"}, + Env: []string{"DEBUG=1"}, + Binary: application.SignedFile{File: "/v23/name/of/binary"}, + } + + // TRANSACTION BEGIN + memstore.Lock() + tname, err := memstore.BindTransactionRoot("ignored").CreateTransaction(nil) + if err != nil { + t.Fatalf("CreateTransaction() failed: %v", err) + } + // Insert a few values + names := []string{"/test/a", "/test/b", "/test/a/x", "/test/a/y", "/test/b/fooooo/bar"} + for _, n := range names { + if _, err := memstore.BindObject(fs.TP(n)).Put(nil, envelope); err != nil { + t.Fatalf("Put() failed: %v", err) + } + } + if err := memstore.BindTransaction(tname).Commit(nil); err != nil { + t.Fatalf("Commit() failed: %v", err) + } + memstore.Unlock() + // TRANSACTION END + + memstore.Lock() + testcases := []struct { + name string + children []string + }{ + {"/", []string{"test"}}, + {"/test", []string{"a", "b"}}, + {"/test/a", []string{"x", "y"}}, + {"/test/b", []string{"fooooo"}}, + {"/test/b/fooooo", []string{"bar"}}, + {"/test/a/x", nil}, + {"/test/a/y", nil}, + } + for _, tc := range testcases { + children, err := memstore.BindObject(tc.name).Children() + if err != nil { + t.Errorf("unexpected error for %q: %v", tc.name, err) + continue + } + if !reflect.DeepEqual(children, tc.children) { + t.Errorf("unexpected result for %q: got %q, expected %q", tc.name, children, tc.children) + } + } + + for _, notthere := range []string{"/doesnt-exist", "/tes"} { + if _, err := memstore.BindObject(notthere).Children(); err == nil { + t.Errorf("unexpected success for: %q", notthere) + } + } + memstore.Unlock() +} + +func TestFormatConversion(t *testing.T) { + path := tempFile(t) + defer os.Remove(path) + originalMemstore, err := fs.NewMemstore(path) + if err != nil { + t.Fatalf("fs.NewMemstore() failed: %v", err) + } + + // Create example data. + envelope := application.Envelope{ + Args: []string{"--help"}, + Env: []string{"DEBUG=1"}, + Binary: application.SignedFile{File: "/v23/name/of/binary"}, + } + + // TRANSACTION BEGIN + // Insert a value into the legacy Memstore at /test/a + originalMemstore.Lock() + tname, err := originalMemstore.BindTransactionRoot("ignored").CreateTransaction(nil) + if err != nil { + t.Fatalf("CreateTransaction() failed: %v", err) + } + if _, err := originalMemstore.BindObject(fs.TP("/test/a")).Put(nil, envelope); err != nil { + t.Fatalf("Put() failed: %v", err) + } + if err := originalMemstore.BindTransaction(tname).Commit(nil); err != nil { + t.Fatalf("Commit() failed: %v", err) + } + originalMemstore.Unlock() + + // Write the original memstore to a GOB file. + if err := gobPersist(t, originalMemstore); err != nil { + t.Fatalf("gobPersist() failed: %v", err) + } + + // Open the GOB format file. + memstore, err := fs.NewMemstore(path) + if err != nil { + t.Fatalf("fs.NewMemstore() failed: %v", err) + } + + // Verify the state. + if err := allPathsEqual(memstore, []PathValue{{"/test/a", envelope}}); err != nil { + t.Fatalf("%v", err) + } +} + +// gobPersist writes Memstore ms to its backing file. +func gobPersist(t *testing.T, ms *fs.Memstore) error { + // Convert this memstore to the legacy GOM format. + data := ms.GetGOBConvertedMemstore() + + // Persist this file to a GOB format file. + fname := ms.PersistedFile() + file, err := os.Create(fname) + if err != nil { + t.Fatalf("os.Create(%s) failed: %v", fname, err) + } + defer file.Close() + + enc := gob.NewEncoder(file) + err = enc.Encode(data) + if err := enc.Encode(data); err != nil { + t.Fatalf("enc.Encode() failed: %v", err) + } + return nil +} From c3785ec711a7320f8c6525c60e3bd2fee47aa611 Mon Sep 17 00:00:00 2001 From: Razvan Musaloiu-E <razvanm@google.com> Date: Fri, 4 Jan 2019 15:16:53 -0800 Subject: [PATCH 5/5] Fix the tests for v.io/x/ref/services/internal/binarylib --- x/ref/services/internal/binarylib/client_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/x/ref/services/internal/binarylib/client_test.go b/x/ref/services/internal/binarylib/client_test.go index 89729a513..71e3cff17 100644 --- a/x/ref/services/internal/binarylib/client_test.go +++ b/x/ref/services/internal/binarylib/client_test.go @@ -20,8 +20,14 @@ import ( "v.io/x/ref/services/internal/packages" "v.io/x/ref/test" "v.io/x/ref/test/testutil" + "v.io/x/ref/runtime/factories/library" ) +func init() { + // Allow v23.Init to be called multiple times. + library.AllowMultipleInitializations = true +} + const ( v23Prefix = "vanadium_binary_repository" )