Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 1 | package impl |
| 2 | |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 3 | // The implementation of the node manager expects that the node |
| 4 | // manager installations are all organized in the following directory |
| 5 | // structure: |
| 6 | // |
| 7 | // VEYRON_NM_ROOT/ |
| 8 | // current # symlink to one of the workspaces |
| 9 | // workspace-1/ |
| 10 | // noded - the node manager binary |
| 11 | // noded.sh - a shell script to start the binary |
| 12 | // ... |
| 13 | // workspace-n/ |
| 14 | // noded - the node manager binary |
| 15 | // noded.sh - a shell script to start the binary |
| 16 | // |
| 17 | // The node manager is always expected to be started through |
| 18 | // VEYRON_NM_ROOT/current/noded.sh, which is monitored by an init |
| 19 | // daemon. This provides for simple and robust updates. |
| 20 | // |
| 21 | // To update the node manager to a newer version, a new workspace is |
| 22 | // created and the "current" symlink is updated |
| 23 | // accordingly. Similarly, to revert the node manager to a previous |
| 24 | // version, all that is required is to update the symlink to point to |
| 25 | // the previous workspace. |
| 26 | |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 27 | import ( |
| 28 | "bytes" |
| 29 | "errors" |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 30 | "fmt" |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 31 | "io/ioutil" |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 32 | "math/rand" |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 33 | "os" |
| 34 | "os/exec" |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 35 | "path/filepath" |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 36 | "reflect" |
| 37 | "regexp" |
| 38 | "runtime" |
| 39 | "strings" |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 40 | "sync" |
Jiri Simsa | 4b968d9 | 2014-05-22 18:36:51 -0700 | [diff] [blame] | 41 | "time" |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 42 | |
Bogdan Caprita | a4d9ee4 | 2014-06-20 16:42:53 -0700 | [diff] [blame] | 43 | "veyron/lib/config" |
Cosmos Nicolaou | 019270a | 2014-05-23 08:03:07 -0700 | [diff] [blame] | 44 | vexec "veyron/lib/exec" |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 45 | ibuild "veyron/services/mgmt/build" |
Jiri Simsa | 51d78fc | 2014-07-09 18:34:08 -0700 | [diff] [blame] | 46 | "veyron/services/mgmt/lib/binary" |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 47 | "veyron/services/mgmt/profile" |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 48 | |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 49 | "veyron2/ipc" |
Bogdan Caprita | a4d9ee4 | 2014-06-20 16:42:53 -0700 | [diff] [blame] | 50 | "veyron2/mgmt" |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 51 | "veyron2/naming" |
Jiri Simsa | 4b968d9 | 2014-05-22 18:36:51 -0700 | [diff] [blame] | 52 | "veyron2/rt" |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 53 | "veyron2/services/mgmt/application" |
| 54 | "veyron2/services/mgmt/build" |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 55 | "veyron2/services/mgmt/node" |
Jiri Simsa | ddbfebb | 2014-06-20 15:56:06 -0700 | [diff] [blame] | 56 | "veyron2/services/mgmt/repository" |
Bogdan Caprita | 55c1012 | 2014-07-09 15:35:07 -0700 | [diff] [blame] | 57 | "veyron2/verror" |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 58 | "veyron2/vlog" |
| 59 | ) |
| 60 | |
Jiri Simsa | a95daec | 2014-06-20 15:51:59 -0700 | [diff] [blame] | 61 | const CurrentWorkspace = "current" |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 62 | |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 63 | // state wraps state shared between different node manager |
| 64 | // invocations. |
| 65 | type state struct { |
| 66 | // channels maps callback identifiers to channels that are used to |
| 67 | // communicate information from child processes. |
| 68 | channels map[string]chan string |
| 69 | // channelsMutex is a lock for coordinating concurrent access to |
| 70 | // <channels>. |
| 71 | channelsMutex *sync.Mutex |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 72 | // envelope is the node manager application envelope. |
| 73 | envelope *application.Envelope |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 74 | // name is the node manager name. |
| 75 | name string |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 76 | // previous holds the local path to the previous version of the node |
| 77 | // manager. |
| 78 | previous string |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 79 | // updating is a flag that records whether this instance of node |
| 80 | // manager is being updated. |
| 81 | updating bool |
| 82 | // updatingMutex is a lock for coordinating concurrent access to |
| 83 | // <updating>. |
| 84 | updatingMutex *sync.Mutex |
| 85 | } |
| 86 | |
| 87 | // invoker holds the state of a node manager invocation. |
| 88 | type invoker struct { |
| 89 | state *state |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 90 | // suffix is the suffix of the current invocation that is assumed to |
Bogdan Caprita | d9281a3 | 2014-07-02 14:40:39 -0700 | [diff] [blame] | 91 | // be used as a relative object name to identify an application, |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 92 | // installation, or instance. |
| 93 | suffix string |
| 94 | } |
| 95 | |
| 96 | var ( |
Bogdan Caprita | 55c1012 | 2014-07-09 15:35:07 -0700 | [diff] [blame] | 97 | appsSuffix = regexp.MustCompile(`^apps\/.*$`) |
| 98 | |
| 99 | errInvalidSuffix = verror.BadArgf("invalid suffix") |
| 100 | errOperationFailed = verror.Internalf("operation failed") |
| 101 | errUpdateInProgress = verror.Existsf("update in progress") |
| 102 | errIncompatibleUpdate = verror.BadArgf("update failed: mismatching app title") |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 103 | ) |
| 104 | |
| 105 | // NewInvoker is the invoker factory. |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 106 | func NewInvoker(state *state, suffix string) *invoker { |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 107 | return &invoker{ |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 108 | state: state, |
| 109 | suffix: suffix, |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 110 | } |
| 111 | } |
| 112 | |
| 113 | // NODE INTERFACE IMPLEMENTATION |
| 114 | |
| 115 | // computeNodeProfile generates a description of the runtime |
| 116 | // environment (supported file format, OS, architecture, libraries) of |
| 117 | // the host node. |
| 118 | // |
| 119 | // TODO(jsimsa): Avoid computing the host node description from |
| 120 | // scratch if a recent cached copy exists. |
| 121 | func (i *invoker) computeNodeProfile() (*profile.Specification, error) { |
| 122 | result := profile.Specification{Format: profile.Format{Attributes: make(map[string]string)}} |
| 123 | |
| 124 | // Find out what the supported file format, operating system, and |
| 125 | // architecture is. |
| 126 | switch runtime.GOOS { |
| 127 | case "linux": |
| 128 | result.Format.Name = ibuild.ELF.String() |
| 129 | result.Format.Attributes["os"] = ibuild.LINUX.String() |
| 130 | case "darwin": |
| 131 | result.Format.Name = ibuild.MACH.String() |
| 132 | result.Format.Attributes["os"] = ibuild.DARWIN.String() |
| 133 | case "windows": |
| 134 | result.Format.Name = ibuild.PE.String() |
| 135 | result.Format.Attributes["os"] = ibuild.WINDOWS.String() |
| 136 | default: |
| 137 | return nil, errors.New("Unsupported operating system: " + runtime.GOOS) |
| 138 | } |
| 139 | switch runtime.GOARCH { |
| 140 | case "amd64": |
| 141 | result.Format.Attributes["arch"] = ibuild.AMD64.String() |
| 142 | case "arm": |
| 143 | result.Format.Attributes["arch"] = ibuild.AMD64.String() |
| 144 | case "x86": |
| 145 | result.Format.Attributes["arch"] = ibuild.AMD64.String() |
| 146 | default: |
| 147 | return nil, errors.New("Unsupported hardware architecture: " + runtime.GOARCH) |
| 148 | } |
| 149 | |
| 150 | // Find out what the installed dynamically linked libraries are. |
| 151 | switch runtime.GOOS { |
| 152 | case "linux": |
| 153 | // For Linux, we identify what dynamically linked libraries are |
| 154 | // install by parsing the output of "ldconfig -p". |
| 155 | command := exec.Command("ldconfig", "-p") |
| 156 | output, err := command.CombinedOutput() |
| 157 | if err != nil { |
| 158 | return nil, err |
| 159 | } |
| 160 | buf := bytes.NewBuffer(output) |
| 161 | // Throw away the first line of output from ldconfig. |
| 162 | if _, err := buf.ReadString('\n'); err != nil { |
| 163 | return nil, errors.New("Could not identify libraries.") |
| 164 | } |
| 165 | // Extract the library name and version from every subsequent line. |
| 166 | result.Libraries = make(map[profile.Library]struct{}) |
| 167 | line, err := buf.ReadString('\n') |
| 168 | for err == nil { |
| 169 | words := strings.Split(strings.Trim(line, " \t\n"), " ") |
| 170 | if len(words) > 0 { |
| 171 | tokens := strings.Split(words[0], ".so") |
| 172 | if len(tokens) != 2 { |
| 173 | return nil, errors.New("Could not identify library: " + words[0]) |
| 174 | } |
| 175 | name := strings.TrimPrefix(tokens[0], "lib") |
| 176 | major, minor := "", "" |
| 177 | tokens = strings.SplitN(tokens[1], ".", 3) |
| 178 | if len(tokens) >= 2 { |
| 179 | major = tokens[1] |
| 180 | } |
| 181 | if len(tokens) >= 3 { |
| 182 | minor = tokens[2] |
| 183 | } |
| 184 | result.Libraries[profile.Library{Name: name, MajorVersion: major, MinorVersion: minor}] = struct{}{} |
| 185 | } |
| 186 | line, err = buf.ReadString('\n') |
| 187 | } |
| 188 | case "darwin": |
| 189 | // TODO(jsimsa): Implement. |
| 190 | case "windows": |
| 191 | // TODO(jsimsa): Implement. |
| 192 | default: |
| 193 | return nil, errors.New("Unsupported operating system: " + runtime.GOOS) |
| 194 | } |
| 195 | return &result, nil |
| 196 | } |
| 197 | |
| 198 | // getProfile gets a profile description for the given profile. |
| 199 | // |
| 200 | // TODO(jsimsa): Avoid retrieving the list of known profiles from a |
| 201 | // remote server if a recent cached copy exists. |
| 202 | func (i *invoker) getProfile(name string) (*profile.Specification, error) { |
| 203 | // TODO(jsimsa): This function assumes the existence of a profile |
| 204 | // server from which the profiles can be retrieved. The profile |
| 205 | // server is a work in progress. When it exists, the commented out |
| 206 | // code below should work. |
| 207 | var profile profile.Specification |
| 208 | /* |
| 209 | client, err := r.NewClient() |
| 210 | if err != nil { |
| 211 | vlog.Errorf("NewClient() failed: %v", err) |
| 212 | return nil, err |
| 213 | } |
| 214 | defer client.Close() |
| 215 | server := // TODO |
| 216 | method := "Specification" |
| 217 | inputs := make([]interface{}, 0) |
| 218 | call, err := client.StartCall(server + "/" + name, method, inputs) |
| 219 | if err != nil { |
| 220 | vlog.Errorf("StartCall(%s, %q, %v) failed: %v\n", server + "/" + name, method, inputs, err) |
| 221 | return nil, err |
| 222 | } |
| 223 | if err := call.Finish(&profiles); err != nil { |
| 224 | vlog.Errorf("Finish(%v) failed: %v\n", &profiles, err) |
| 225 | return nil, err |
| 226 | } |
| 227 | */ |
| 228 | return &profile, nil |
| 229 | } |
| 230 | |
| 231 | // getKnownProfiles gets a list of description for all publicly known |
| 232 | // profiles. |
| 233 | // |
| 234 | // TODO(jsimsa): Avoid retrieving the list of known profiles from a |
| 235 | // remote server if a recent cached copy exists. |
| 236 | func (i *invoker) getKnownProfiles() ([]profile.Specification, error) { |
| 237 | // TODO(jsimsa): This function assumes the existence of a profile |
| 238 | // server from which a list of known profiles can be retrieved. The |
| 239 | // profile server is a work in progress. When it exists, the |
| 240 | // commented out code below should work. |
| 241 | knownProfiles := make([]profile.Specification, 0) |
| 242 | /* |
| 243 | client, err := r.NewClient() |
| 244 | if err != nil { |
| 245 | vlog.Errorf("NewClient() failed: %v\n", err) |
| 246 | return nil, err |
| 247 | } |
| 248 | defer client.Close() |
| 249 | server := // TODO |
| 250 | method := "List" |
| 251 | inputs := make([]interface{}, 0) |
| 252 | call, err := client.StartCall(server, method, inputs) |
| 253 | if err != nil { |
| 254 | vlog.Errorf("StartCall(%s, %q, %v) failed: %v\n", server, method, inputs, err) |
| 255 | return nil, err |
| 256 | } |
| 257 | if err := call.Finish(&knownProfiles); err != nil { |
| 258 | vlog.Errorf("Finish(&knownProfile) failed: %v\n", err) |
| 259 | return nil, err |
| 260 | } |
| 261 | */ |
| 262 | return knownProfiles, nil |
| 263 | } |
| 264 | |
| 265 | // matchProfiles inputs a profile that describes the host node and a |
| 266 | // set of publicly known profiles and outputs a node description that |
| 267 | // identifies the publicly known profiles supported by the host node. |
| 268 | func (i *invoker) matchProfiles(p *profile.Specification, known []profile.Specification) node.Description { |
| 269 | result := node.Description{Profiles: make(map[string]struct{})} |
| 270 | loop: |
| 271 | for _, profile := range known { |
| 272 | if profile.Format.Name != p.Format.Name { |
| 273 | continue |
| 274 | } |
| 275 | if profile.Format.Attributes["os"] != p.Format.Attributes["os"] { |
| 276 | continue |
| 277 | } |
| 278 | if profile.Format.Attributes["arch"] != p.Format.Attributes["arch"] { |
| 279 | continue |
| 280 | } |
| 281 | for library := range profile.Libraries { |
| 282 | // Current implementation requires exact library name and version match. |
| 283 | if _, found := p.Libraries[library]; !found { |
| 284 | continue loop |
| 285 | } |
| 286 | } |
| 287 | result.Profiles[profile.Label] = struct{}{} |
| 288 | } |
| 289 | return result |
| 290 | } |
| 291 | |
Matt Rosencrantz | f5afcaf | 2014-06-02 11:31:22 -0700 | [diff] [blame] | 292 | func (i *invoker) Describe(call ipc.ServerContext) (node.Description, error) { |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 293 | vlog.VI(0).Infof("%v.Describe()", i.suffix) |
| 294 | empty := node.Description{} |
| 295 | nodeProfile, err := i.computeNodeProfile() |
| 296 | if err != nil { |
| 297 | return empty, err |
| 298 | } |
| 299 | knownProfiles, err := i.getKnownProfiles() |
| 300 | if err != nil { |
| 301 | return empty, err |
| 302 | } |
| 303 | result := i.matchProfiles(nodeProfile, knownProfiles) |
| 304 | return result, nil |
| 305 | } |
| 306 | |
Matt Rosencrantz | f5afcaf | 2014-06-02 11:31:22 -0700 | [diff] [blame] | 307 | func (i *invoker) IsRunnable(call ipc.ServerContext, binary build.BinaryDescription) (bool, error) { |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 308 | vlog.VI(0).Infof("%v.IsRunnable(%v)", i.suffix, binary) |
| 309 | nodeProfile, err := i.computeNodeProfile() |
| 310 | if err != nil { |
| 311 | return false, err |
| 312 | } |
| 313 | binaryProfiles := make([]profile.Specification, 0) |
| 314 | for name, _ := range binary.Profiles { |
| 315 | profile, err := i.getProfile(name) |
| 316 | if err != nil { |
| 317 | return false, err |
| 318 | } |
| 319 | binaryProfiles = append(binaryProfiles, *profile) |
| 320 | } |
| 321 | result := i.matchProfiles(nodeProfile, binaryProfiles) |
| 322 | return len(result.Profiles) > 0, nil |
| 323 | } |
| 324 | |
Matt Rosencrantz | f5afcaf | 2014-06-02 11:31:22 -0700 | [diff] [blame] | 325 | func (i *invoker) Reset(call ipc.ServerContext, deadline uint64) error { |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 326 | vlog.VI(0).Infof("%v.Reset(%v)", i.suffix, deadline) |
| 327 | // TODO(jsimsa): Implement. |
| 328 | return nil |
| 329 | } |
| 330 | |
| 331 | // APPLICATION INTERFACE IMPLEMENTATION |
| 332 | |
Jiri Simsa | 51d78fc | 2014-07-09 18:34:08 -0700 | [diff] [blame] | 333 | func downloadBinary(workspace, name string) error { |
| 334 | data, err := binary.Download(name) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 335 | if err != nil { |
Jiri Simsa | 51d78fc | 2014-07-09 18:34:08 -0700 | [diff] [blame] | 336 | vlog.Errorf("Download(%v) failed: %v", name, err) |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 337 | return errOperationFailed |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 338 | } |
Jiri Simsa | 51d78fc | 2014-07-09 18:34:08 -0700 | [diff] [blame] | 339 | path, perm := filepath.Join(workspace, "noded"), os.FileMode(755) |
| 340 | if err := ioutil.WriteFile(path, data, perm); err != nil { |
| 341 | vlog.Errorf("WriteFile(%v, %v) failed: %v", path, perm, err) |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 342 | return errOperationFailed |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 343 | } |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 344 | return nil |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 345 | } |
| 346 | |
| 347 | func fetchEnvelope(origin string) (*application.Envelope, error) { |
Jiri Simsa | ddbfebb | 2014-06-20 15:56:06 -0700 | [diff] [blame] | 348 | stub, err := repository.BindApplication(origin) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 349 | if err != nil { |
| 350 | vlog.Errorf("BindRepository(%v) failed: %v", origin, err) |
| 351 | return nil, errOperationFailed |
| 352 | } |
| 353 | // TODO(jsimsa): Include logic that computes the set of supported |
| 354 | // profiles. |
Jiri Simsa | 52567a0 | 2014-05-22 15:25:23 -0700 | [diff] [blame] | 355 | profiles := []string{"test"} |
Matt Rosencrantz | f5afcaf | 2014-06-02 11:31:22 -0700 | [diff] [blame] | 356 | envelope, err := stub.Match(rt.R().NewContext(), profiles) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 357 | if err != nil { |
| 358 | vlog.Errorf("Match(%v) failed: %v", profiles, err) |
| 359 | return nil, errOperationFailed |
| 360 | } |
| 361 | return &envelope, nil |
| 362 | } |
| 363 | |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 364 | func generateBinary(workspace string, envelope *application.Envelope, newBinary bool) error { |
| 365 | if newBinary { |
| 366 | // Download the new binary. |
| 367 | return downloadBinary(workspace, envelope.Binary) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 368 | } |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 369 | // Link the current binary. |
| 370 | path := filepath.Join(workspace, "noded") |
| 371 | if err := os.Link(os.Args[0], path); err != nil { |
| 372 | vlog.Errorf("Link(%v, %v) failed: %v", os.Args[0], path, err) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 373 | return errOperationFailed |
| 374 | } |
| 375 | return nil |
| 376 | } |
| 377 | |
Jiri Simsa | a95daec | 2014-06-20 15:51:59 -0700 | [diff] [blame] | 378 | // TODO(jsimsa): Replace <PreviousEnv> with a command-line flag when |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 379 | // command-line flags in tests are supported. |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 380 | func generateScript(workspace string, envelope *application.Envelope) error { |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 381 | path, err := filepath.EvalSymlinks(os.Args[0]) |
| 382 | if err != nil { |
| 383 | vlog.Errorf("EvalSymlinks(%v) failed: %v", os.Args[0], err) |
| 384 | return errOperationFailed |
| 385 | } |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 386 | output := "#!/bin/bash\n" |
Jiri Simsa | a95daec | 2014-06-20 15:51:59 -0700 | [diff] [blame] | 387 | output += PreviousEnv + "=" + filepath.Dir(path) + " " |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 388 | output += strings.Join(envelope.Env, " ") + " " |
Jiri Simsa | 1adc1ba | 2014-07-10 16:08:08 -0700 | [diff] [blame] | 389 | output += filepath.Join(workspace, "noded") + " " |
| 390 | output += strings.Join(envelope.Args, " ") |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 391 | path = filepath.Join(workspace, "noded.sh") |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 392 | if err := ioutil.WriteFile(path, []byte(output), 0755); err != nil { |
| 393 | vlog.Errorf("WriteFile(%v) failed: %v", path, err) |
| 394 | return errOperationFailed |
| 395 | } |
| 396 | return nil |
| 397 | } |
| 398 | |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 399 | // getCurrentFileInfo returns the os.FileInfo for both the symbolic |
| 400 | // link $VEYRON_NM_ROOT/current and the workspace this link points to. |
| 401 | func getCurrentFileInfo() (os.FileInfo, os.FileInfo, error) { |
Jiri Simsa | a95daec | 2014-06-20 15:51:59 -0700 | [diff] [blame] | 402 | path := filepath.Join(os.Getenv(RootEnv), CurrentWorkspace) |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 403 | link, err := os.Lstat(path) |
| 404 | if err != nil { |
| 405 | vlog.Errorf("Lstat(%v) failed: %v", path, err) |
| 406 | return nil, nil, err |
| 407 | } |
| 408 | workspace, err := os.Stat(path) |
| 409 | if err != nil { |
| 410 | vlog.Errorf("Stat(%v) failed: %v", path, err) |
| 411 | return nil, nil, err |
| 412 | } |
| 413 | return link, workspace, nil |
| 414 | } |
| 415 | |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 416 | func updateLink(workspace string) error { |
Jiri Simsa | a95daec | 2014-06-20 15:51:59 -0700 | [diff] [blame] | 417 | link := filepath.Join(os.Getenv(RootEnv), CurrentWorkspace) |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 418 | newLink := link + ".new" |
| 419 | fi, err := os.Lstat(newLink) |
| 420 | if err == nil { |
| 421 | if err := os.Remove(fi.Name()); err != nil { |
| 422 | vlog.Errorf("Remove(%v) failed: %v", fi.Name(), err) |
| 423 | return errOperationFailed |
| 424 | } |
| 425 | } |
| 426 | if err := os.Symlink(workspace, newLink); err != nil { |
| 427 | vlog.Errorf("Symlink(%v, %v) failed: %v", workspace, newLink, err) |
| 428 | return errOperationFailed |
| 429 | } |
| 430 | if err := os.Rename(newLink, link); err != nil { |
| 431 | vlog.Errorf("Rename(%v, %v) failed: %v", newLink, link, err) |
| 432 | return errOperationFailed |
| 433 | } |
| 434 | return nil |
| 435 | } |
| 436 | |
| 437 | func (i *invoker) registerCallback(id string, channel chan string) { |
| 438 | i.state.channelsMutex.Lock() |
| 439 | defer i.state.channelsMutex.Unlock() |
| 440 | i.state.channels[id] = channel |
| 441 | } |
| 442 | |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 443 | func (i *invoker) revertNodeManager() error { |
| 444 | if err := updateLink(i.state.previous); err != nil { |
| 445 | return err |
| 446 | } |
| 447 | rt.R().Stop() |
| 448 | return nil |
| 449 | } |
| 450 | |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 451 | func (i *invoker) testNodeManager(workspace string, envelope *application.Envelope) error { |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 452 | path := filepath.Join(workspace, "noded.sh") |
| 453 | cmd := exec.Command(path) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 454 | cmd.Stdout = os.Stdout |
| 455 | cmd.Stderr = os.Stderr |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 456 | // Setup up the child process callback. |
| 457 | id := fmt.Sprintf("%d", rand.Int()) |
Bogdan Caprita | a4d9ee4 | 2014-06-20 16:42:53 -0700 | [diff] [blame] | 458 | cfg := config.New() |
| 459 | cfg.Set(mgmt.ParentNodeManagerConfigKey, naming.JoinAddressName(i.state.name, id)) |
| 460 | handle := vexec.NewParentHandle(cmd, vexec.ConfigOpt{cfg}) |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 461 | callbackChan := make(chan string) |
| 462 | i.registerCallback(id, callbackChan) |
| 463 | defer i.unregisterCallback(id) |
| 464 | // Start the child process. |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 465 | if err := handle.Start(); err != nil { |
| 466 | vlog.Errorf("Start() failed: %v", err) |
| 467 | return errOperationFailed |
| 468 | } |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 469 | // Wait for the child process to start. |
Jenkins Veyron | 7a7704a | 2014-06-12 00:51:14 +0000 | [diff] [blame] | 470 | testTimeout := 10 * time.Second |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 471 | if err := handle.WaitForReady(testTimeout); err != nil { |
| 472 | vlog.Errorf("WaitForReady(%v) failed: %v", testTimeout, err) |
| 473 | if err := cmd.Process.Kill(); err != nil { |
| 474 | vlog.Errorf("Kill() failed: %v", err) |
| 475 | } |
Jiri Simsa | 4b968d9 | 2014-05-22 18:36:51 -0700 | [diff] [blame] | 476 | return errOperationFailed |
| 477 | } |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 478 | // Wait for the child process to invoke the Callback(). |
| 479 | select { |
| 480 | case address := <-callbackChan: |
| 481 | // Check that invoking Update() succeeds. |
| 482 | address = naming.JoinAddressName(address, "nm") |
| 483 | nmClient, err := node.BindNode(address) |
| 484 | if err != nil { |
| 485 | vlog.Errorf("BindNode(%v) failed: %v", address, err) |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 486 | if err := handle.Clean(); err != nil { |
| 487 | vlog.Errorf("Clean() failed: %v", err) |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 488 | } |
| 489 | return errOperationFailed |
| 490 | } |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 491 | linkOld, workspaceOld, err := getCurrentFileInfo() |
| 492 | if err != nil { |
| 493 | if err := handle.Clean(); err != nil { |
| 494 | vlog.Errorf("Clean() failed: %v", err) |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 495 | } |
| 496 | return errOperationFailed |
| 497 | } |
Jiri Simsa | 76e3459 | 2014-06-25 14:24:06 -0700 | [diff] [blame] | 498 | // Since the resolution of mtime for files is seconds, |
| 499 | // the test sleeps for a second to make sure it can |
| 500 | // check whether the "current" symlink is updated. |
| 501 | time.Sleep(time.Second) |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 502 | if err := nmClient.Revert(rt.R().NewContext()); err != nil { |
| 503 | if err := handle.Clean(); err != nil { |
| 504 | vlog.Errorf("Clean() failed: %v", err) |
| 505 | } |
| 506 | return errOperationFailed |
| 507 | } |
| 508 | linkNew, workspaceNew, err := getCurrentFileInfo() |
| 509 | if err != nil { |
| 510 | if err := handle.Clean(); err != nil { |
| 511 | vlog.Errorf("Clean() failed: %v", err) |
| 512 | } |
| 513 | return errOperationFailed |
| 514 | } |
| 515 | // Check that the new node manager updated the symbolic link |
| 516 | // $VEYRON_NM_ROOT/current. |
| 517 | if !linkOld.ModTime().Before(linkNew.ModTime()) { |
| 518 | vlog.Errorf("new node manager test failed") |
| 519 | return errOperationFailed |
| 520 | } |
| 521 | // Check that the symbolic link $VEYRON_NM_ROOT/current points to |
| 522 | // the same workspace. |
| 523 | if workspaceOld.Name() != workspaceNew.Name() { |
| 524 | updateLink(workspaceOld.Name()) |
| 525 | vlog.Errorf("new node manager test failed") |
| 526 | return errOperationFailed |
| 527 | } |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 528 | case <-time.After(testTimeout): |
| 529 | vlog.Errorf("Waiting for callback timed out") |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 530 | if err := handle.Clean(); err != nil { |
| 531 | vlog.Errorf("Clean() failed: %v", err) |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 532 | } |
| 533 | return errOperationFailed |
| 534 | } |
| 535 | return nil |
| 536 | } |
| 537 | |
| 538 | func (i *invoker) unregisterCallback(id string) { |
| 539 | i.state.channelsMutex.Lock() |
| 540 | defer i.state.channelsMutex.Unlock() |
| 541 | delete(i.state.channels, id) |
| 542 | } |
| 543 | |
| 544 | func (i *invoker) updateNodeManager() error { |
Jiri Simsa | a95daec | 2014-06-20 15:51:59 -0700 | [diff] [blame] | 545 | envelope, err := fetchEnvelope(os.Getenv(OriginEnv)) |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 546 | if err != nil { |
| 547 | return err |
| 548 | } |
Bogdan Caprita | 55c1012 | 2014-07-09 15:35:07 -0700 | [diff] [blame] | 549 | if envelope.Title != application.NodeManagerTitle { |
| 550 | return errIncompatibleUpdate |
| 551 | } |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 552 | if !reflect.DeepEqual(envelope, i.state.envelope) { |
| 553 | // Create new workspace. |
Jiri Simsa | a95daec | 2014-06-20 15:51:59 -0700 | [diff] [blame] | 554 | workspace := filepath.Join(os.Getenv(RootEnv), fmt.Sprintf("%v", time.Now().Format(time.RFC3339Nano))) |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 555 | perm := os.FileMode(0755) |
| 556 | if err := os.MkdirAll(workspace, perm); err != nil { |
| 557 | vlog.Errorf("MkdirAll(%v, %v) failed: %v", workspace, perm, err) |
| 558 | return errOperationFailed |
| 559 | } |
| 560 | // Populate the new workspace with a node manager binary. |
| 561 | if err := generateBinary(workspace, envelope, envelope.Binary != i.state.envelope.Binary); err != nil { |
| 562 | if err := os.RemoveAll(workspace); err != nil { |
| 563 | vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err) |
| 564 | } |
| 565 | return err |
| 566 | } |
| 567 | // Populate the new workspace with a node manager script. |
| 568 | if err := generateScript(workspace, envelope); err != nil { |
| 569 | if err := os.RemoveAll(workspace); err != nil { |
| 570 | vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err) |
| 571 | } |
| 572 | return err |
| 573 | } |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 574 | if err := i.testNodeManager(workspace, envelope); err != nil { |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 575 | if err := os.RemoveAll(workspace); err != nil { |
| 576 | vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err) |
| 577 | } |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 578 | return err |
| 579 | } |
| 580 | // If the binary has changed, update the node manager symlink. |
| 581 | if err := updateLink(workspace); err != nil { |
| 582 | if err := os.RemoveAll(workspace); err != nil { |
| 583 | vlog.Errorf("RemoveAll(%v) failed: %v", workspace, err) |
| 584 | } |
| 585 | return err |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 586 | } |
| 587 | rt.R().Stop() |
| 588 | } |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 589 | return nil |
| 590 | } |
| 591 | |
Bogdan Caprita | 55c1012 | 2014-07-09 15:35:07 -0700 | [diff] [blame] | 592 | func (i *invoker) Install(call ipc.ServerContext, von string) (string, error) { |
| 593 | vlog.VI(0).Infof("%v.Install(%q)", i.suffix, von) |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 594 | // TODO(jsimsa): Implement. |
| 595 | return "", nil |
| 596 | } |
| 597 | |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 598 | func (i *invoker) Refresh(call ipc.ServerContext) error { |
| 599 | vlog.VI(0).Infof("%v.Refresh()", i.suffix) |
| 600 | // TODO(jsimsa): Implement. |
| 601 | return nil |
| 602 | } |
| 603 | |
| 604 | func (i *invoker) Restart(call ipc.ServerContext) error { |
| 605 | vlog.VI(0).Infof("%v.Restart()", i.suffix) |
| 606 | // TODO(jsimsa): Implement. |
| 607 | return nil |
| 608 | } |
| 609 | |
| 610 | func (i *invoker) Resume(call ipc.ServerContext) error { |
| 611 | vlog.VI(0).Infof("%v.Resume()", i.suffix) |
| 612 | // TODO(jsimsa): Implement. |
| 613 | return nil |
| 614 | } |
| 615 | |
| 616 | func (i *invoker) Revert(call ipc.ServerContext) error { |
| 617 | vlog.VI(0).Infof("%v.Revert()", i.suffix) |
| 618 | if i.state.previous == "" { |
| 619 | return errOperationFailed |
| 620 | } |
| 621 | i.state.updatingMutex.Lock() |
| 622 | if i.state.updating { |
| 623 | i.state.updatingMutex.Unlock() |
| 624 | return errUpdateInProgress |
| 625 | } else { |
| 626 | i.state.updating = true |
| 627 | } |
| 628 | i.state.updatingMutex.Unlock() |
| 629 | err := i.revertNodeManager() |
| 630 | i.state.updatingMutex.Lock() |
| 631 | i.state.updating = false |
| 632 | i.state.updatingMutex.Unlock() |
| 633 | return err |
| 634 | } |
| 635 | |
Matt Rosencrantz | f5afcaf | 2014-06-02 11:31:22 -0700 | [diff] [blame] | 636 | func (i *invoker) Start(call ipc.ServerContext) ([]string, error) { |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 637 | vlog.VI(0).Infof("%v.Start()", i.suffix) |
| 638 | // TODO(jsimsa): Implement. |
| 639 | return make([]string, 0), nil |
| 640 | } |
| 641 | |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 642 | func (i *invoker) Stop(call ipc.ServerContext, deadline uint64) error { |
| 643 | vlog.VI(0).Infof("%v.Stop(%v)", i.suffix, deadline) |
| 644 | // TODO(jsimsa): Implement. |
| 645 | return nil |
| 646 | } |
| 647 | |
| 648 | func (i *invoker) Suspend(call ipc.ServerContext) error { |
| 649 | vlog.VI(0).Infof("%v.Suspend()", i.suffix) |
| 650 | // TODO(jsimsa): Implement. |
| 651 | return nil |
| 652 | } |
| 653 | |
Matt Rosencrantz | f5afcaf | 2014-06-02 11:31:22 -0700 | [diff] [blame] | 654 | func (i *invoker) Uninstall(call ipc.ServerContext) error { |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 655 | vlog.VI(0).Infof("%v.Uninstall()", i.suffix) |
| 656 | // TODO(jsimsa): Implement. |
| 657 | return nil |
| 658 | } |
| 659 | |
Matt Rosencrantz | f5afcaf | 2014-06-02 11:31:22 -0700 | [diff] [blame] | 660 | func (i *invoker) Update(call ipc.ServerContext) error { |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 661 | vlog.VI(0).Infof("%v.Update()", i.suffix) |
| 662 | switch { |
| 663 | case i.suffix == "nm": |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 664 | // This branch attempts to update the node manager itself. |
| 665 | i.state.updatingMutex.Lock() |
| 666 | if i.state.updating { |
| 667 | i.state.updatingMutex.Unlock() |
| 668 | return errUpdateInProgress |
| 669 | } else { |
| 670 | i.state.updating = true |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 671 | } |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 672 | i.state.updatingMutex.Unlock() |
| 673 | err := i.updateNodeManager() |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 674 | i.state.updatingMutex.Lock() |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 675 | i.state.updating = false |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 676 | i.state.updatingMutex.Unlock() |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 677 | return err |
Jiri Simsa | 70c3205 | 2014-06-18 11:38:21 -0700 | [diff] [blame] | 678 | case appsSuffix.MatchString(i.suffix): |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 679 | // TODO(jsimsa): Implement. |
| 680 | return nil |
| 681 | default: |
| 682 | return errInvalidSuffix |
| 683 | } |
Bogdan Caprita | 55c1012 | 2014-07-09 15:35:07 -0700 | [diff] [blame] | 684 | |
| 685 | } |
| 686 | |
| 687 | func (i *invoker) UpdateTo(call ipc.ServerContext, von string) error { |
| 688 | vlog.VI(0).Infof("%v.UpdateTo(%q)", i.suffix, von) |
| 689 | // TODO(jsimsa): Implement. |
| 690 | return nil |
Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 691 | } |
| 692 | |
Bogdan Caprita | a4d9ee4 | 2014-06-20 16:42:53 -0700 | [diff] [blame] | 693 | // CONFIG INTERFACE IMPLEMENTATION |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 694 | |
Bogdan Caprita | a4d9ee4 | 2014-06-20 16:42:53 -0700 | [diff] [blame] | 695 | func (i *invoker) Set(_ ipc.ServerContext, key, value string) error { |
| 696 | vlog.VI(0).Infof("%v.Set(%v, %v)", i.suffix, key, value) |
| 697 | // For now, only handle the child node manager name. We'll add handling |
| 698 | // for the child's app cycle manager name later on. |
| 699 | if key != mgmt.ChildNodeManagerConfigKey { |
| 700 | return nil |
| 701 | } |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 702 | i.state.channelsMutex.Lock() |
| 703 | channel, ok := i.state.channels[i.suffix] |
| 704 | i.state.channelsMutex.Unlock() |
| 705 | if !ok { |
| 706 | return errInvalidSuffix |
| 707 | } |
Bogdan Caprita | a4d9ee4 | 2014-06-20 16:42:53 -0700 | [diff] [blame] | 708 | channel <- value |
Jiri Simsa | 24e87aa | 2014-06-09 09:27:34 -0700 | [diff] [blame] | 709 | return nil |
| 710 | } |