blob: 21874fc6b7d12a30fb4ba1c77a921609cbb5cf04 [file] [log] [blame]
Jiri Simsa5293dcb2014-05-10 09:56:38 -07001package impl
2
3import (
4 "bytes"
5 "errors"
6 "io"
7 "io/ioutil"
8 "os"
9 "os/exec"
10 "reflect"
11 "regexp"
12 "runtime"
13 "strings"
14 "syscall"
15
16 vexec "veyron/runtimes/google/lib/exec"
17 ibuild "veyron/services/mgmt/build"
18 "veyron/services/mgmt/profile"
19 "veyron2/ipc"
20 "veyron2/services/mgmt/application"
21 "veyron2/services/mgmt/build"
22 "veyron2/services/mgmt/content"
23 "veyron2/services/mgmt/node"
24 "veyron2/vlog"
25)
26
27var updateSuffix = regexp.MustCompile(`^apps\/.*$`)
28
29// invoker holds the state of a node manager invocation.
30type invoker struct {
31 // envelope is the node manager application envelope.
32 envelope *application.Envelope
33 // origin is a veyron name that resolves to the node manager
34 // envelope.
35 origin string
36 // suffix is the suffix of the current invocation that is assumed to
37 // be used as a relative veyron name to identify an application,
38 // installation, or instance.
39 suffix string
40}
41
42var (
43 errInvalidSuffix = errors.New("invalid suffix")
44 errOperationFailed = errors.New("operation failed")
45)
46
47// NewInvoker is the invoker factory.
48func NewInvoker(envelope *application.Envelope, origin, suffix string) *invoker {
49 return &invoker{
50 envelope: envelope,
51 origin: origin,
52 suffix: suffix,
53 }
54}
55
56// NODE INTERFACE IMPLEMENTATION
57
58// computeNodeProfile generates a description of the runtime
59// environment (supported file format, OS, architecture, libraries) of
60// the host node.
61//
62// TODO(jsimsa): Avoid computing the host node description from
63// scratch if a recent cached copy exists.
64func (i *invoker) computeNodeProfile() (*profile.Specification, error) {
65 result := profile.Specification{Format: profile.Format{Attributes: make(map[string]string)}}
66
67 // Find out what the supported file format, operating system, and
68 // architecture is.
69 switch runtime.GOOS {
70 case "linux":
71 result.Format.Name = ibuild.ELF.String()
72 result.Format.Attributes["os"] = ibuild.LINUX.String()
73 case "darwin":
74 result.Format.Name = ibuild.MACH.String()
75 result.Format.Attributes["os"] = ibuild.DARWIN.String()
76 case "windows":
77 result.Format.Name = ibuild.PE.String()
78 result.Format.Attributes["os"] = ibuild.WINDOWS.String()
79 default:
80 return nil, errors.New("Unsupported operating system: " + runtime.GOOS)
81 }
82 switch runtime.GOARCH {
83 case "amd64":
84 result.Format.Attributes["arch"] = ibuild.AMD64.String()
85 case "arm":
86 result.Format.Attributes["arch"] = ibuild.AMD64.String()
87 case "x86":
88 result.Format.Attributes["arch"] = ibuild.AMD64.String()
89 default:
90 return nil, errors.New("Unsupported hardware architecture: " + runtime.GOARCH)
91 }
92
93 // Find out what the installed dynamically linked libraries are.
94 switch runtime.GOOS {
95 case "linux":
96 // For Linux, we identify what dynamically linked libraries are
97 // install by parsing the output of "ldconfig -p".
98 command := exec.Command("ldconfig", "-p")
99 output, err := command.CombinedOutput()
100 if err != nil {
101 return nil, err
102 }
103 buf := bytes.NewBuffer(output)
104 // Throw away the first line of output from ldconfig.
105 if _, err := buf.ReadString('\n'); err != nil {
106 return nil, errors.New("Could not identify libraries.")
107 }
108 // Extract the library name and version from every subsequent line.
109 result.Libraries = make(map[profile.Library]struct{})
110 line, err := buf.ReadString('\n')
111 for err == nil {
112 words := strings.Split(strings.Trim(line, " \t\n"), " ")
113 if len(words) > 0 {
114 tokens := strings.Split(words[0], ".so")
115 if len(tokens) != 2 {
116 return nil, errors.New("Could not identify library: " + words[0])
117 }
118 name := strings.TrimPrefix(tokens[0], "lib")
119 major, minor := "", ""
120 tokens = strings.SplitN(tokens[1], ".", 3)
121 if len(tokens) >= 2 {
122 major = tokens[1]
123 }
124 if len(tokens) >= 3 {
125 minor = tokens[2]
126 }
127 result.Libraries[profile.Library{Name: name, MajorVersion: major, MinorVersion: minor}] = struct{}{}
128 }
129 line, err = buf.ReadString('\n')
130 }
131 case "darwin":
132 // TODO(jsimsa): Implement.
133 case "windows":
134 // TODO(jsimsa): Implement.
135 default:
136 return nil, errors.New("Unsupported operating system: " + runtime.GOOS)
137 }
138 return &result, nil
139}
140
141// getProfile gets a profile description for the given profile.
142//
143// TODO(jsimsa): Avoid retrieving the list of known profiles from a
144// remote server if a recent cached copy exists.
145func (i *invoker) getProfile(name string) (*profile.Specification, error) {
146 // TODO(jsimsa): This function assumes the existence of a profile
147 // server from which the profiles can be retrieved. The profile
148 // server is a work in progress. When it exists, the commented out
149 // code below should work.
150 var profile profile.Specification
151 /*
152 client, err := r.NewClient()
153 if err != nil {
154 vlog.Errorf("NewClient() failed: %v", err)
155 return nil, err
156 }
157 defer client.Close()
158 server := // TODO
159 method := "Specification"
160 inputs := make([]interface{}, 0)
161 call, err := client.StartCall(server + "/" + name, method, inputs)
162 if err != nil {
163 vlog.Errorf("StartCall(%s, %q, %v) failed: %v\n", server + "/" + name, method, inputs, err)
164 return nil, err
165 }
166 if err := call.Finish(&profiles); err != nil {
167 vlog.Errorf("Finish(%v) failed: %v\n", &profiles, err)
168 return nil, err
169 }
170 */
171 return &profile, nil
172}
173
174// getKnownProfiles gets a list of description for all publicly known
175// profiles.
176//
177// TODO(jsimsa): Avoid retrieving the list of known profiles from a
178// remote server if a recent cached copy exists.
179func (i *invoker) getKnownProfiles() ([]profile.Specification, error) {
180 // TODO(jsimsa): This function assumes the existence of a profile
181 // server from which a list of known profiles can be retrieved. The
182 // profile server is a work in progress. When it exists, the
183 // commented out code below should work.
184 knownProfiles := make([]profile.Specification, 0)
185 /*
186 client, err := r.NewClient()
187 if err != nil {
188 vlog.Errorf("NewClient() failed: %v\n", err)
189 return nil, err
190 }
191 defer client.Close()
192 server := // TODO
193 method := "List"
194 inputs := make([]interface{}, 0)
195 call, err := client.StartCall(server, method, inputs)
196 if err != nil {
197 vlog.Errorf("StartCall(%s, %q, %v) failed: %v\n", server, method, inputs, err)
198 return nil, err
199 }
200 if err := call.Finish(&knownProfiles); err != nil {
201 vlog.Errorf("Finish(&knownProfile) failed: %v\n", err)
202 return nil, err
203 }
204 */
205 return knownProfiles, nil
206}
207
208// matchProfiles inputs a profile that describes the host node and a
209// set of publicly known profiles and outputs a node description that
210// identifies the publicly known profiles supported by the host node.
211func (i *invoker) matchProfiles(p *profile.Specification, known []profile.Specification) node.Description {
212 result := node.Description{Profiles: make(map[string]struct{})}
213loop:
214 for _, profile := range known {
215 if profile.Format.Name != p.Format.Name {
216 continue
217 }
218 if profile.Format.Attributes["os"] != p.Format.Attributes["os"] {
219 continue
220 }
221 if profile.Format.Attributes["arch"] != p.Format.Attributes["arch"] {
222 continue
223 }
224 for library := range profile.Libraries {
225 // Current implementation requires exact library name and version match.
226 if _, found := p.Libraries[library]; !found {
227 continue loop
228 }
229 }
230 result.Profiles[profile.Label] = struct{}{}
231 }
232 return result
233}
234
235func (i *invoker) Describe(call ipc.Context) (node.Description, error) {
236 vlog.VI(0).Infof("%v.Describe()", i.suffix)
237 empty := node.Description{}
238 nodeProfile, err := i.computeNodeProfile()
239 if err != nil {
240 return empty, err
241 }
242 knownProfiles, err := i.getKnownProfiles()
243 if err != nil {
244 return empty, err
245 }
246 result := i.matchProfiles(nodeProfile, knownProfiles)
247 return result, nil
248}
249
250func (i *invoker) IsRunnable(call ipc.Context, binary build.BinaryDescription) (bool, error) {
251 vlog.VI(0).Infof("%v.IsRunnable(%v)", i.suffix, binary)
252 nodeProfile, err := i.computeNodeProfile()
253 if err != nil {
254 return false, err
255 }
256 binaryProfiles := make([]profile.Specification, 0)
257 for name, _ := range binary.Profiles {
258 profile, err := i.getProfile(name)
259 if err != nil {
260 return false, err
261 }
262 binaryProfiles = append(binaryProfiles, *profile)
263 }
264 result := i.matchProfiles(nodeProfile, binaryProfiles)
265 return len(result.Profiles) > 0, nil
266}
267
268func (i *invoker) Reset(call ipc.Context, deadline uint64) error {
269 vlog.VI(0).Infof("%v.Reset(%v)", i.suffix, deadline)
270 // TODO(jsimsa): Implement.
271 return nil
272}
273
274// APPLICATION INTERFACE IMPLEMENTATION
275
276func downloadBinary(binary string) (string, error) {
277 stub, err := content.BindContent(binary)
278 if err != nil {
279 vlog.Errorf("BindContent(%q) failed: %v", binary, err)
280 return "", errOperationFailed
281 }
282 stream, err := stub.Download()
283 if err != nil {
284 vlog.Errorf("Download() failed: %v", err)
285 return "", errOperationFailed
286 }
287 tmpDir, prefix := "", ""
288 file, err := ioutil.TempFile(tmpDir, prefix)
289 if err != nil {
290 vlog.Errorf("TempFile(%q, %q) failed: %v", tmpDir, prefix, err)
291 return "", errOperationFailed
292 }
293 defer file.Close()
294 for {
295 bytes, err := stream.Recv()
296 if err == io.EOF {
297 break
298 }
299 if err != nil {
300 vlog.Errorf("Recv() failed: %v", err)
301 os.Remove(file.Name())
302 return "", errOperationFailed
303 }
304 if _, err := file.Write(bytes); err != nil {
305 vlog.Errorf("Write() failed: %v", err)
306 os.Remove(file.Name())
307 return "", errOperationFailed
308 }
309 }
310 if err := stream.Finish(); err != nil {
311 vlog.Errorf("Finish() failed: %v", err)
312 os.Remove(file.Name())
313 return "", errOperationFailed
314 }
315 mode := os.FileMode(0755)
316 if err := file.Chmod(mode); err != nil {
317 vlog.Errorf("Chmod(%v) failed: %v", mode, err)
318 os.Remove(file.Name())
319 return "", errOperationFailed
320 }
321 return file.Name(), nil
322}
323
324func fetchEnvelope(origin string) (*application.Envelope, error) {
325 stub, err := application.BindRepository(origin)
326 if err != nil {
327 vlog.Errorf("BindRepository(%v) failed: %v", origin, err)
328 return nil, errOperationFailed
329 }
330 // TODO(jsimsa): Include logic that computes the set of supported
331 // profiles.
Jiri Simsa52567a02014-05-22 15:25:23 -0700332 profiles := []string{"test"}
Jiri Simsa5293dcb2014-05-10 09:56:38 -0700333 envelope, err := stub.Match(profiles)
334 if err != nil {
335 vlog.Errorf("Match(%v) failed: %v", profiles, err)
336 return nil, errOperationFailed
337 }
338 return &envelope, nil
339}
340
341func replaceBinary(oldBinary, newBinary string) error {
342 // Replace the old binary with the new one.
343 if err := syscall.Unlink(oldBinary); err != nil {
344 vlog.Errorf("Unlink(%v) failed: %v", oldBinary, err)
345 return errOperationFailed
346 }
347 if err := os.Rename(newBinary, oldBinary); err != nil {
348 vlog.Errorf("Rename(%v, %v) failed: %v", newBinary, oldBinary, err)
349 return errOperationFailed
350 }
351 return nil
352}
353
354func spawnNodeManager(envelope *application.Envelope) error {
355 cmd := exec.Command(os.Args[0], envelope.Args...)
356 cmd.Env = envelope.Env
357 cmd.Stdout = os.Stdout
358 cmd.Stderr = os.Stderr
359 handle := vexec.NewParentHandle(cmd, "")
360 if err := handle.Start(); err != nil {
361 vlog.Errorf("Start() failed: %v", err)
362 return errOperationFailed
363 }
364 return nil
365}
366
367func (i *invoker) Install(call ipc.Context) (string, error) {
368 vlog.VI(0).Infof("%v.Install()", i.suffix)
369 // TODO(jsimsa): Implement.
370 return "", nil
371}
372
373func (i *invoker) Start(call ipc.Context) ([]string, error) {
374 vlog.VI(0).Infof("%v.Start()", i.suffix)
375 // TODO(jsimsa): Implement.
376 return make([]string, 0), nil
377}
378
379func (i *invoker) Uninstall(call ipc.Context) error {
380 vlog.VI(0).Infof("%v.Uninstall()", i.suffix)
381 // TODO(jsimsa): Implement.
382 return nil
383}
384
385func (i *invoker) Update(call ipc.Context) error {
386 vlog.VI(0).Infof("%v.Update()", i.suffix)
387 switch {
388 case i.suffix == "nm":
389 // This branch attempts to update the node manager updates itself.
390 envelope, err := fetchEnvelope(i.origin)
391 if err != nil {
392 return err
393 }
394 if envelope.Binary != i.envelope.Binary {
395 file, err := downloadBinary(envelope.Binary)
396 if err != nil {
397 return err
398 }
399 if err := replaceBinary(os.Args[0], file); err != nil {
400 os.Remove(file)
401 return err
402 }
403 }
404 if !reflect.DeepEqual(envelope, i.envelope) {
405 i.envelope = envelope
406 if err := spawnNodeManager(i.envelope); err != nil {
407 return err
408 }
409 // TODO(jsimsa): When Bogdan implements the shutdown API, use it
410 // to stop itself (or have the caller do that).
411 }
412 return nil
413 case updateSuffix.MatchString(i.suffix):
414 // TODO(jsimsa): Implement.
415 return nil
416 default:
417 return errInvalidSuffix
418 }
419}
420
421func (i *invoker) Refresh(call ipc.Context) error {
422 vlog.VI(0).Infof("%v.Refresh()", i.suffix)
423 // TODO(jsimsa): Implement.
424 return nil
425}
426
427func (i *invoker) Restart(call ipc.Context) error {
428 vlog.VI(0).Infof("%v.Restart()", i.suffix)
429 // TODO(jsimsa): Implement.
430 return nil
431}
432
433func (i *invoker) Resume(call ipc.Context) error {
434 vlog.VI(0).Infof("%v.Resume()", i.suffix)
435 // TODO(jsimsa): Implement.
436 return nil
437}
438
439func (i *invoker) Shutdown(call ipc.Context, deadline uint64) error {
440 vlog.VI(0).Infof("%v.Shutdown(%v)", i.suffix, deadline)
441 // TODO(jsimsa): Implement.
442 return nil
443}
444
445func (i *invoker) Suspend(call ipc.Context) error {
446 vlog.VI(0).Infof("%v.Suspend()", i.suffix)
447 // TODO(jsimsa): Implement.
448 return nil
449}