Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 1 | package impl |
| 2 | |
| 3 | import ( |
| 4 | "crypto/md5" |
| 5 | "encoding/hex" |
| 6 | "fmt" |
| 7 | "io" |
| 8 | "io/ioutil" |
| 9 | "os" |
Bogdan Caprita | 50e2153 | 2015-02-06 17:33:27 -0800 | [diff] [blame] | 10 | "path/filepath" |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 11 | "strings" |
| 12 | |
Jiri Simsa | 6ac9522 | 2015-02-23 16:11:49 -0800 | [diff] [blame] | 13 | "v.io/v23" |
| 14 | "v.io/v23/context" |
| 15 | "v.io/v23/ipc" |
| 16 | "v.io/v23/naming" |
| 17 | "v.io/v23/security" |
| 18 | "v.io/v23/services/mgmt/application" |
| 19 | "v.io/v23/services/mgmt/binary" |
| 20 | "v.io/v23/services/mgmt/device" |
| 21 | "v.io/v23/services/mgmt/repository" |
| 22 | "v.io/v23/services/security/access" |
| 23 | "v.io/v23/uniqueid" |
| 24 | "v.io/v23/vlog" |
Bogdan Caprita | 50e2153 | 2015-02-06 17:33:27 -0800 | [diff] [blame] | 25 | |
Bogdan Caprita | 047fa45 | 2015-02-11 10:18:04 -0800 | [diff] [blame] | 26 | pkglib "v.io/core/veyron/services/mgmt/lib/packages" |
Jiri Simsa | 24a7155 | 2015-02-27 11:31:36 -0800 | [diff] [blame^] | 27 | "v.io/x/lib/cmdline" |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 28 | ) |
| 29 | |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 30 | var cmdInstallLocal = &cmdline.Command{ |
| 31 | Run: runInstallLocal, |
| 32 | Name: "install-local", |
| 33 | Short: "Install the given application from the local system.", |
| 34 | Long: "Install the given application, specified using a local path.", |
Bogdan Caprita | 50e2153 | 2015-02-06 17:33:27 -0800 | [diff] [blame] | 35 | ArgsName: "<device> <title> [ENV=VAL ...] binary [--flag=val ...] [PACKAGES path ...]", |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 36 | ArgsLong: ` |
| 37 | <device> is the veyron object name of the device manager's app service. |
| 38 | |
| 39 | <title> is the app title. |
| 40 | |
| 41 | This is followed by an arbitrary number of environment variable settings, the |
Bogdan Caprita | 50e2153 | 2015-02-06 17:33:27 -0800 | [diff] [blame] | 42 | local path for the binary to install, and arbitrary flag settings and args. |
| 43 | Optionally, this can be followed by 'PACKAGES' and a list of local files and |
| 44 | directories to be installed as packages for the app`} |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 45 | |
Bogdan Caprita | 8964d3f | 2015-02-02 13:47:39 -0800 | [diff] [blame] | 46 | func init() { |
| 47 | cmdInstallLocal.Flags.Var(&configOverride, "config", "JSON-encoded device.Config object, of the form: '{\"flag1\":\"value1\",\"flag2\":\"value2\"}'") |
Bogdan Caprita | f5b9441 | 2015-02-17 10:15:21 -0800 | [diff] [blame] | 48 | cmdInstallLocal.Flags.Var(&packagesOverride, "packages", "JSON-encoded application.Packages object, of the form: '{\"pkg1\":{\"File\":\"local file path1\"},\"pkg2\":{\"File\":\"local file path 2\"}}'") |
Bogdan Caprita | 8964d3f | 2015-02-02 13:47:39 -0800 | [diff] [blame] | 49 | } |
| 50 | |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 51 | type openAuthorizer struct{} |
| 52 | |
| 53 | func (openAuthorizer) Authorize(security.Context) error { return nil } |
| 54 | |
| 55 | type mapDispatcher map[string]interface{} |
| 56 | |
| 57 | func (d mapDispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) { |
| 58 | o, ok := d[suffix] |
| 59 | if !ok { |
| 60 | return nil, nil, fmt.Errorf("suffix %s not found", suffix) |
| 61 | } |
| 62 | // TODO(caprita): Do not open authorizer even for a short-lived server. |
| 63 | return o, &openAuthorizer{}, nil |
| 64 | } |
| 65 | |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 66 | type mapServer struct { |
| 67 | name string |
| 68 | dispatcher mapDispatcher |
| 69 | } |
| 70 | |
| 71 | func (ms *mapServer) serve(name string, object interface{}) (string, error) { |
| 72 | if _, ok := ms.dispatcher[name]; ok { |
| 73 | return "", fmt.Errorf("can't have more than one object with name %v", name) |
| 74 | } |
| 75 | ms.dispatcher[name] = object |
| 76 | return naming.Join(ms.name, name), nil |
| 77 | } |
| 78 | |
| 79 | func createServer(ctx *context.T, stderr io.Writer) (*mapServer, func(), error) { |
Jiri Simsa | 6ac9522 | 2015-02-23 16:11:49 -0800 | [diff] [blame] | 80 | server, err := v23.NewServer(ctx) |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 81 | if err != nil { |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 82 | return nil, nil, err |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 83 | } |
Jiri Simsa | 6ac9522 | 2015-02-23 16:11:49 -0800 | [diff] [blame] | 84 | spec := v23.GetListenSpec(ctx) |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 85 | endpoints, err := server.Listen(spec) |
| 86 | if err != nil { |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 87 | return nil, nil, err |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 88 | } |
| 89 | var name string |
| 90 | if spec.Proxy != "" { |
| 91 | id, err := uniqueid.Random() |
| 92 | if err != nil { |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 93 | return nil, nil, err |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 94 | } |
| 95 | name = id.String() |
| 96 | } |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 97 | dispatcher := make(mapDispatcher) |
| 98 | if err := server.ServeDispatcher(name, dispatcher); err != nil { |
| 99 | return nil, nil, err |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 100 | } |
Bogdan Caprita | 2250a3c | 2015-02-19 21:50:13 -0800 | [diff] [blame] | 101 | vlog.VI(1).Infof("Server listening on %v (%v)", endpoints, name) |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 102 | cleanup := func() { |
| 103 | if err := server.Stop(); err != nil { |
| 104 | fmt.Fprintf(stderr, "server.Stop failed: %v", err) |
| 105 | } |
| 106 | } |
| 107 | if name != "" { |
| 108 | // Send a name rooted in our namespace root rather than the |
| 109 | // relative name (in case the device manager uses a different |
| 110 | // namespace root). |
| 111 | // |
| 112 | // TODO(caprita): Avoid relying on a mounttable altogether, and |
| 113 | // instead pull out the proxied address and just send that. |
Jiri Simsa | 6ac9522 | 2015-02-23 16:11:49 -0800 | [diff] [blame] | 114 | nsRoots := v23.GetNamespace(ctx).Roots() |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 115 | if len(nsRoots) > 0 { |
| 116 | name = naming.Join(nsRoots[0], name) |
| 117 | } |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 118 | } else if len(endpoints) > 0 { |
| 119 | name = endpoints[0].Name() |
| 120 | } else { |
| 121 | return nil, nil, fmt.Errorf("no endpoints") |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 122 | } |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 123 | return &mapServer{name: name, dispatcher: dispatcher}, cleanup, nil |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 124 | } |
| 125 | |
| 126 | var errNotImplemented = fmt.Errorf("method not implemented") |
| 127 | |
| 128 | type binaryInvoker string |
| 129 | |
| 130 | func (binaryInvoker) Create(ipc.ServerContext, int32, repository.MediaInfo) error { |
| 131 | return errNotImplemented |
| 132 | } |
| 133 | |
| 134 | func (binaryInvoker) Delete(ipc.ServerContext) error { |
| 135 | return errNotImplemented |
| 136 | } |
| 137 | |
| 138 | func (i binaryInvoker) Download(ctx repository.BinaryDownloadContext, _ int32) error { |
| 139 | fileName := string(i) |
| 140 | file, err := os.Open(fileName) |
| 141 | if err != nil { |
| 142 | return err |
| 143 | } |
| 144 | defer file.Close() |
| 145 | bufferLength := 4096 |
| 146 | buffer := make([]byte, bufferLength) |
| 147 | sender := ctx.SendStream() |
| 148 | for { |
| 149 | n, err := file.Read(buffer) |
| 150 | switch err { |
| 151 | case io.EOF: |
| 152 | return nil |
| 153 | case nil: |
| 154 | if err := sender.Send(buffer[:n]); err != nil { |
| 155 | return err |
| 156 | } |
| 157 | default: |
| 158 | return err |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | func (binaryInvoker) DownloadURL(ipc.ServerContext) (string, int64, error) { |
| 164 | return "", 0, errNotImplemented |
| 165 | } |
| 166 | |
| 167 | func (i binaryInvoker) Stat(ctx ipc.ServerContext) ([]binary.PartInfo, repository.MediaInfo, error) { |
| 168 | fileName := string(i) |
| 169 | h := md5.New() |
| 170 | bytes, err := ioutil.ReadFile(fileName) |
| 171 | if err != nil { |
| 172 | return []binary.PartInfo{}, repository.MediaInfo{}, err |
| 173 | } |
| 174 | h.Write(bytes) |
| 175 | part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))} |
Bogdan Caprita | 047fa45 | 2015-02-11 10:18:04 -0800 | [diff] [blame] | 176 | return []binary.PartInfo{part}, pkglib.MediaInfoForFileName(fileName), nil |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 177 | } |
| 178 | |
| 179 | func (binaryInvoker) Upload(repository.BinaryUploadContext, int32) error { |
| 180 | return errNotImplemented |
| 181 | } |
| 182 | |
| 183 | func (binaryInvoker) GetACL(ctx ipc.ServerContext) (acl access.TaggedACLMap, etag string, err error) { |
| 184 | return nil, "", errNotImplemented |
| 185 | } |
| 186 | |
| 187 | func (binaryInvoker) SetACL(ctx ipc.ServerContext, acl access.TaggedACLMap, etag string) error { |
| 188 | return errNotImplemented |
| 189 | } |
| 190 | |
| 191 | type envelopeInvoker application.Envelope |
| 192 | |
| 193 | func (i envelopeInvoker) Match(ipc.ServerContext, []string) (application.Envelope, error) { |
| 194 | return application.Envelope(i), nil |
| 195 | } |
| 196 | func (envelopeInvoker) GetACL(ipc.ServerContext) (acl access.TaggedACLMap, etag string, err error) { |
| 197 | return nil, "", errNotImplemented |
| 198 | } |
| 199 | |
| 200 | func (envelopeInvoker) SetACL(ipc.ServerContext, access.TaggedACLMap, string) error { |
| 201 | return errNotImplemented |
| 202 | } |
| 203 | |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 204 | func servePackage(p string, ms *mapServer, tmpZipDir string) (string, string, error) { |
| 205 | info, err := os.Stat(p) |
| 206 | if os.IsNotExist(err) { |
| 207 | return "", "", fmt.Errorf("%v not found: %v", p, err) |
| 208 | } else if err != nil { |
| 209 | return "", "", fmt.Errorf("Stat(%v) failed: %v", p, err) |
| 210 | } |
| 211 | pkgName := naming.Join("packages", info.Name()) |
| 212 | fileName := p |
| 213 | // Directory packages first get zip'ped. |
| 214 | if info.IsDir() { |
| 215 | fileName = filepath.Join(tmpZipDir, info.Name()+".zip") |
Bogdan Caprita | 047fa45 | 2015-02-11 10:18:04 -0800 | [diff] [blame] | 216 | if err := pkglib.CreateZip(fileName, p); err != nil { |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 217 | return "", "", err |
| 218 | } |
| 219 | } |
| 220 | name, err := ms.serve(pkgName, repository.BinaryServer(binaryInvoker(fileName))) |
| 221 | return info.Name(), name, err |
| 222 | } |
| 223 | |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 224 | // runInstallLocal creates a new envelope on the fly from the provided |
| 225 | // arguments, and then points the device manager back to itself for downloading |
| 226 | // the app envelope and binary. |
| 227 | // |
| 228 | // It sets up an app and binary server that only lives for the duration of the |
| 229 | // command, and listens on the profile's listen spec. The caller should set the |
| 230 | // --veyron.proxy if the machine running the command is not accessible from the |
| 231 | // device manager. |
| 232 | // |
| 233 | // TODO(caprita/ashankar): We should use bi-directional streams to get this |
| 234 | // working over the same connection that the command makes to the device |
| 235 | // manager. |
| 236 | func runInstallLocal(cmd *cmdline.Command, args []string) error { |
| 237 | if expectedMin, got := 2, len(args); got < expectedMin { |
| 238 | return cmd.UsageErrorf("install-local: incorrect number of arguments, expected at least %d, got %d", expectedMin, got) |
| 239 | } |
| 240 | deviceName, title := args[0], args[1] |
| 241 | args = args[2:] |
| 242 | envelope := application.Envelope{Title: title} |
| 243 | // Extract the environment settings, binary, and arguments. |
| 244 | firstNonEnv := len(args) |
| 245 | for i, arg := range args { |
| 246 | if strings.Index(arg, "=") <= 0 { |
| 247 | firstNonEnv = i |
| 248 | break |
| 249 | } |
| 250 | } |
| 251 | envelope.Env = args[:firstNonEnv] |
| 252 | args = args[firstNonEnv:] |
| 253 | if len(args) == 0 { |
| 254 | return cmd.UsageErrorf("install-local: missing binary") |
| 255 | } |
| 256 | binary := args[0] |
Bogdan Caprita | 50e2153 | 2015-02-06 17:33:27 -0800 | [diff] [blame] | 257 | args = args[1:] |
| 258 | firstNonArg, firstPackage := len(args), len(args) |
| 259 | for i, arg := range args { |
| 260 | if arg == "PACKAGES" { |
| 261 | firstNonArg = i |
| 262 | firstPackage = i + 1 |
| 263 | break |
| 264 | } |
| 265 | } |
| 266 | envelope.Args = args[:firstNonArg] |
| 267 | pkgs := args[firstPackage:] |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 268 | if _, err := os.Stat(binary); err != nil { |
| 269 | return fmt.Errorf("binary %v not found: %v", binary, err) |
| 270 | } |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 271 | server, cancel, err := createServer(gctx, cmd.Stderr()) |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 272 | if err != nil { |
| 273 | return fmt.Errorf("failed to create server: %v", err) |
| 274 | } |
| 275 | defer cancel() |
Bogdan Caprita | c25a48c | 2015-02-12 13:45:51 -0800 | [diff] [blame] | 276 | envelope.Binary.File, err = server.serve("binary", repository.BinaryServer(binaryInvoker(binary))) |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 277 | if err != nil { |
| 278 | return err |
| 279 | } |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 280 | |
Bogdan Caprita | 50e2153 | 2015-02-06 17:33:27 -0800 | [diff] [blame] | 281 | // For each package dir/file specified in the arguments list, set up an |
| 282 | // object in the binary service to serve that package, and add the |
| 283 | // object name to the envelope's Packages map. |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 284 | tmpZipDir, err := ioutil.TempDir("", "packages") |
| 285 | if err != nil { |
| 286 | return fmt.Errorf("failed to create a temp dir for zip packages: %v", err) |
| 287 | } |
| 288 | defer os.RemoveAll(tmpZipDir) |
Bogdan Caprita | 50e2153 | 2015-02-06 17:33:27 -0800 | [diff] [blame] | 289 | for _, p := range pkgs { |
| 290 | if envelope.Packages == nil { |
Bogdan Caprita | c25a48c | 2015-02-12 13:45:51 -0800 | [diff] [blame] | 291 | envelope.Packages = make(application.Packages) |
Bogdan Caprita | 50e2153 | 2015-02-06 17:33:27 -0800 | [diff] [blame] | 292 | } |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 293 | pname, oname, err := servePackage(p, server, tmpZipDir) |
| 294 | if err != nil { |
| 295 | return err |
Bogdan Caprita | 50e2153 | 2015-02-06 17:33:27 -0800 | [diff] [blame] | 296 | } |
Bogdan Caprita | 2250a3c | 2015-02-19 21:50:13 -0800 | [diff] [blame] | 297 | vlog.VI(1).Infof("package %v serving as %v", pname, oname) |
Bogdan Caprita | c25a48c | 2015-02-12 13:45:51 -0800 | [diff] [blame] | 298 | envelope.Packages[pname] = application.SignedFile{File: oname} |
Bogdan Caprita | 50e2153 | 2015-02-06 17:33:27 -0800 | [diff] [blame] | 299 | } |
Bogdan Caprita | 047fa45 | 2015-02-11 10:18:04 -0800 | [diff] [blame] | 300 | packagesRewritten := application.Packages{} |
| 301 | for pname, pspec := range packagesOverride { |
| 302 | _, oname, err := servePackage(pspec.File, server, tmpZipDir) |
| 303 | if err != nil { |
| 304 | return err |
| 305 | } |
Bogdan Caprita | 2250a3c | 2015-02-19 21:50:13 -0800 | [diff] [blame] | 306 | vlog.VI(1).Infof("package %v serving as %v", pname, oname) |
Bogdan Caprita | 047fa45 | 2015-02-11 10:18:04 -0800 | [diff] [blame] | 307 | pspec.File = oname |
| 308 | packagesRewritten[pname] = pspec |
| 309 | } |
Bogdan Caprita | 20088dd | 2015-02-09 19:01:48 -0800 | [diff] [blame] | 310 | appName, err := server.serve("application", repository.ApplicationServer(envelopeInvoker(envelope))) |
| 311 | if err != nil { |
| 312 | return err |
| 313 | } |
Bogdan Caprita | 2250a3c | 2015-02-19 21:50:13 -0800 | [diff] [blame] | 314 | vlog.VI(1).Infof("application serving envelope as %v", appName) |
Bogdan Caprita | 047fa45 | 2015-02-11 10:18:04 -0800 | [diff] [blame] | 315 | appID, err := device.ApplicationClient(deviceName).Install(gctx, appName, device.Config(configOverride), packagesRewritten) |
Bogdan Caprita | 8964d3f | 2015-02-02 13:47:39 -0800 | [diff] [blame] | 316 | // Reset the value for any future invocations of "install" or |
| 317 | // "install-local" (we run more than one command per process in unit |
| 318 | // tests). |
| 319 | configOverride = configFlag{} |
Bogdan Caprita | 047fa45 | 2015-02-11 10:18:04 -0800 | [diff] [blame] | 320 | packagesOverride = packagesFlag{} |
Bogdan Caprita | b646a1c | 2015-01-30 19:16:37 -0800 | [diff] [blame] | 321 | if err != nil { |
| 322 | return fmt.Errorf("Install failed: %v", err) |
| 323 | } |
| 324 | fmt.Fprintf(cmd.Stdout(), "Successfully installed: %q\n", naming.Join(deviceName, appID)) |
| 325 | return nil |
| 326 | } |