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