github.com/ryanslade/nomad@v0.2.4-0.20160128061903-fc95782f2089/client/driver/executor/exec_linux.go (about) 1 package executor 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "os" 9 "os/exec" 10 "os/user" 11 "path/filepath" 12 "strconv" 13 "strings" 14 "sync" 15 "syscall" 16 17 "github.com/hashicorp/go-multierror" 18 "github.com/opencontainers/runc/libcontainer/cgroups" 19 cgroupFs "github.com/opencontainers/runc/libcontainer/cgroups/fs" 20 "github.com/opencontainers/runc/libcontainer/cgroups/systemd" 21 cgroupConfig "github.com/opencontainers/runc/libcontainer/configs" 22 23 "github.com/hashicorp/nomad/client/allocdir" 24 "github.com/hashicorp/nomad/client/driver/spawn" 25 cstructs "github.com/hashicorp/nomad/client/driver/structs" 26 "github.com/hashicorp/nomad/nomad/structs" 27 ) 28 29 var ( 30 // A mapping of directories on the host OS to attempt to embed inside each 31 // task's chroot. 32 chrootEnv = map[string]string{ 33 "/bin": "/bin", 34 "/etc": "/etc", 35 "/lib": "/lib", 36 "/lib32": "/lib32", 37 "/lib64": "/lib64", 38 "/usr/bin": "/usr/bin", 39 "/usr/lib": "/usr/lib", 40 "/usr/share": "/usr/share", 41 } 42 ) 43 44 func NewExecutor(ctx *ExecutorContext) Executor { 45 return NewLinuxExecutor(ctx) 46 } 47 48 func NewLinuxExecutor(ctx *ExecutorContext) Executor { 49 return &LinuxExecutor{ExecutorContext: ctx} 50 } 51 52 // Linux executor is designed to run on linux kernel 2.8+. 53 type LinuxExecutor struct { 54 *ExecutorContext 55 cmd exec.Cmd 56 user *user.User 57 l sync.Mutex 58 59 // Isolation configurations. 60 groups *cgroupConfig.Cgroup 61 taskName string 62 taskDir string 63 allocDir string 64 65 // Spawn process. 66 spawn *spawn.Spawner 67 } 68 69 func (e *LinuxExecutor) Command() *exec.Cmd { 70 return &e.cmd 71 } 72 73 func (e *LinuxExecutor) Limit(resources *structs.Resources) error { 74 if resources == nil { 75 return errNoResources 76 } 77 78 return e.configureCgroups(resources) 79 } 80 81 // execLinuxID contains the necessary information to reattach to an executed 82 // process and cleanup the created cgroups. 83 type ExecLinuxID struct { 84 Groups *cgroupConfig.Cgroup 85 Spawn *spawn.Spawner 86 TaskDir string 87 } 88 89 func (e *LinuxExecutor) Open(id string) error { 90 // De-serialize the ID. 91 dec := json.NewDecoder(strings.NewReader(id)) 92 var execID ExecLinuxID 93 if err := dec.Decode(&execID); err != nil { 94 return fmt.Errorf("Failed to parse id: %v", err) 95 } 96 97 // Setup the executor. 98 e.groups = execID.Groups 99 e.spawn = execID.Spawn 100 e.taskDir = execID.TaskDir 101 return e.spawn.Valid() 102 } 103 104 func (e *LinuxExecutor) ID() (string, error) { 105 if e.groups == nil || e.spawn == nil || e.taskDir == "" { 106 return "", fmt.Errorf("LinuxExecutor not properly initialized.") 107 } 108 109 // Build the ID. 110 id := ExecLinuxID{ 111 Groups: e.groups, 112 Spawn: e.spawn, 113 TaskDir: e.taskDir, 114 } 115 116 var buffer bytes.Buffer 117 enc := json.NewEncoder(&buffer) 118 if err := enc.Encode(id); err != nil { 119 return "", fmt.Errorf("Failed to serialize id: %v", err) 120 } 121 122 return buffer.String(), nil 123 } 124 125 // runAs takes a user id as a string and looks up the user, and sets the command 126 // to execute as that user. 127 func (e *LinuxExecutor) runAs(userid string) error { 128 u, err := user.Lookup(userid) 129 if err != nil { 130 return fmt.Errorf("Failed to identify user %v: %v", userid, err) 131 } 132 133 // Convert the uid and gid 134 uid, err := strconv.ParseUint(u.Uid, 10, 32) 135 if err != nil { 136 return fmt.Errorf("Unable to convert userid to uint32: %s", err) 137 } 138 gid, err := strconv.ParseUint(u.Gid, 10, 32) 139 if err != nil { 140 return fmt.Errorf("Unable to convert groupid to uint32: %s", err) 141 } 142 143 // Set the command to run as that user and group. 144 if e.cmd.SysProcAttr == nil { 145 e.cmd.SysProcAttr = &syscall.SysProcAttr{} 146 } 147 if e.cmd.SysProcAttr.Credential == nil { 148 e.cmd.SysProcAttr.Credential = &syscall.Credential{} 149 } 150 e.cmd.SysProcAttr.Credential.Uid = uint32(uid) 151 e.cmd.SysProcAttr.Credential.Gid = uint32(gid) 152 153 return nil 154 } 155 156 func (e *LinuxExecutor) Start() error { 157 // Run as "nobody" user so we don't leak root privilege to the spawned 158 // process. 159 if err := e.runAs("nobody"); err != nil { 160 return err 161 } 162 163 // Parse the commands arguments and replace instances of Nomad environment 164 // variables. 165 e.cmd.Path = e.taskEnv.ReplaceEnv(e.cmd.Path) 166 e.cmd.Args = e.taskEnv.ParseAndReplace(e.cmd.Args) 167 e.cmd.Env = e.taskEnv.EnvList() 168 169 spawnState := filepath.Join(e.allocDir, fmt.Sprintf("%s_%s", e.taskName, "exit_status")) 170 e.spawn = spawn.NewSpawner(spawnState) 171 e.spawn.SetCommand(&e.cmd) 172 e.spawn.SetChroot(e.taskDir) 173 e.spawn.SetLogs(&spawn.Logs{ 174 Stdout: filepath.Join(e.taskDir, allocdir.TaskLocal, fmt.Sprintf("%v.stdout", e.taskName)), 175 Stderr: filepath.Join(e.taskDir, allocdir.TaskLocal, fmt.Sprintf("%v.stderr", e.taskName)), 176 Stdin: os.DevNull, 177 }) 178 179 enterCgroup := func(pid int) error { 180 // Join the spawn-daemon to the cgroup. 181 manager := e.getCgroupManager(e.groups) 182 183 // Apply will place the spawn dameon into the created cgroups. 184 if err := manager.Apply(pid); err != nil { 185 return fmt.Errorf("Failed to join spawn-daemon to the cgroup (%+v): %v", e.groups, err) 186 } 187 188 return nil 189 } 190 191 return e.spawn.Spawn(enterCgroup) 192 } 193 194 // Wait waits til the user process exits and returns an error on non-zero exit 195 // codes. Wait also cleans up the task directory and created cgroups. 196 func (e *LinuxExecutor) Wait() *cstructs.WaitResult { 197 errs := new(multierror.Error) 198 res := e.spawn.Wait() 199 if res.Err != nil { 200 errs = multierror.Append(errs, res.Err) 201 } 202 203 if err := e.destroyCgroup(); err != nil { 204 errs = multierror.Append(errs, err) 205 } 206 207 if err := e.cleanTaskDir(); err != nil { 208 errs = multierror.Append(errs, err) 209 } 210 211 res.Err = errs.ErrorOrNil() 212 return res 213 } 214 215 // Shutdown sends the user process an interrupt signal indicating that it is 216 // about to be forcefully shutdown in sometime 217 func (e *LinuxExecutor) Shutdown() error { 218 proc, err := os.FindProcess(e.spawn.UserPid) 219 if err != nil { 220 return fmt.Errorf("Failed to find user processes %v: %v", e.spawn.UserPid, err) 221 } 222 223 return proc.Signal(os.Interrupt) 224 } 225 226 // ForceStop immediately exits the user process and cleans up both the task 227 // directory and the cgroups. 228 func (e *LinuxExecutor) ForceStop() error { 229 errs := new(multierror.Error) 230 if err := e.destroyCgroup(); err != nil { 231 errs = multierror.Append(errs, err) 232 } 233 234 if err := e.cleanTaskDir(); err != nil { 235 errs = multierror.Append(errs, err) 236 } 237 238 return errs.ErrorOrNil() 239 } 240 241 // Task Directory related functions. 242 243 // ConfigureTaskDir creates the necessary directory structure for a proper 244 // chroot. cleanTaskDir should be called after. 245 func (e *LinuxExecutor) ConfigureTaskDir(taskName string, alloc *allocdir.AllocDir) error { 246 e.taskName = taskName 247 e.allocDir = alloc.AllocDir 248 249 taskDir, ok := alloc.TaskDirs[taskName] 250 if !ok { 251 fmt.Errorf("Couldn't find task directory for task %v", taskName) 252 } 253 e.taskDir = taskDir 254 255 if err := alloc.MountSharedDir(taskName); err != nil { 256 return err 257 } 258 259 if err := alloc.Embed(taskName, chrootEnv); err != nil { 260 return err 261 } 262 263 // Mount dev 264 dev := filepath.Join(taskDir, "dev") 265 if !e.pathExists(dev) { 266 if err := os.Mkdir(dev, 0777); err != nil { 267 return fmt.Errorf("Mkdir(%v) failed: %v", dev, err) 268 } 269 270 if err := syscall.Mount("none", dev, "devtmpfs", syscall.MS_RDONLY, ""); err != nil { 271 return fmt.Errorf("Couldn't mount /dev to %v: %v", dev, err) 272 } 273 } 274 275 // Mount proc 276 proc := filepath.Join(taskDir, "proc") 277 if !e.pathExists(proc) { 278 if err := os.Mkdir(proc, 0777); err != nil { 279 return fmt.Errorf("Mkdir(%v) failed: %v", proc, err) 280 } 281 282 if err := syscall.Mount("none", proc, "proc", syscall.MS_RDONLY, ""); err != nil { 283 return fmt.Errorf("Couldn't mount /proc to %v: %v", proc, err) 284 } 285 } 286 287 // Set the tasks AllocDir environment variable. 288 e.taskEnv.SetAllocDir(filepath.Join("/", allocdir.SharedAllocName)).SetTaskLocalDir(filepath.Join("/", allocdir.TaskLocal)).Build() 289 return nil 290 } 291 292 // pathExists is a helper function to check if the path exists. 293 func (e *LinuxExecutor) pathExists(path string) bool { 294 if _, err := os.Stat(path); err != nil { 295 if os.IsNotExist(err) { 296 return false 297 } 298 } 299 return true 300 } 301 302 // cleanTaskDir is an idempotent operation to clean the task directory and 303 // should be called when tearing down the task. 304 func (e *LinuxExecutor) cleanTaskDir() error { 305 // Prevent a race between Wait/ForceStop 306 e.l.Lock() 307 defer e.l.Unlock() 308 309 // Unmount dev. 310 errs := new(multierror.Error) 311 dev := filepath.Join(e.taskDir, "dev") 312 if e.pathExists(dev) { 313 if err := syscall.Unmount(dev, 0); err != nil { 314 errs = multierror.Append(errs, fmt.Errorf("Failed to unmount dev (%v): %v", dev, err)) 315 } 316 317 if err := os.RemoveAll(dev); err != nil { 318 errs = multierror.Append(errs, fmt.Errorf("Failed to delete dev directory (%v): %v", dev, err)) 319 } 320 } 321 322 // Unmount proc. 323 proc := filepath.Join(e.taskDir, "proc") 324 if e.pathExists(proc) { 325 if err := syscall.Unmount(proc, 0); err != nil { 326 errs = multierror.Append(errs, fmt.Errorf("Failed to unmount proc (%v): %v", proc, err)) 327 } 328 329 if err := os.RemoveAll(proc); err != nil { 330 errs = multierror.Append(errs, fmt.Errorf("Failed to delete proc directory (%v): %v", dev, err)) 331 } 332 } 333 334 return errs.ErrorOrNil() 335 } 336 337 // Cgroup related functions. 338 339 // configureCgroups converts a Nomad Resources specification into the equivalent 340 // cgroup configuration. It returns an error if the resources are invalid. 341 func (e *LinuxExecutor) configureCgroups(resources *structs.Resources) error { 342 e.groups = &cgroupConfig.Cgroup{} 343 e.groups.Resources = &cgroupConfig.Resources{} 344 e.groups.Name = structs.GenerateUUID() 345 346 // TODO: verify this is needed for things like network access 347 e.groups.Resources.AllowAllDevices = true 348 349 if resources.MemoryMB > 0 { 350 // Total amount of memory allowed to consume 351 e.groups.Resources.Memory = int64(resources.MemoryMB * 1024 * 1024) 352 // Disable swap to avoid issues on the machine 353 e.groups.Resources.MemorySwap = int64(-1) 354 } 355 356 if resources.CPU < 2 { 357 return fmt.Errorf("resources.CPU must be equal to or greater than 2: %v", resources.CPU) 358 } 359 360 // Set the relative CPU shares for this cgroup. 361 e.groups.Resources.CpuShares = int64(resources.CPU) 362 363 if resources.IOPS != 0 { 364 // Validate it is in an acceptable range. 365 if resources.IOPS < 10 || resources.IOPS > 1000 { 366 return fmt.Errorf("resources.IOPS must be between 10 and 1000: %d", resources.IOPS) 367 } 368 369 e.groups.Resources.BlkioWeight = uint16(resources.IOPS) 370 } 371 372 return nil 373 } 374 375 // destroyCgroup kills all processes in the cgroup and removes the cgroup 376 // configuration from the host. 377 func (e *LinuxExecutor) destroyCgroup() error { 378 if e.groups == nil { 379 return errors.New("Can't destroy: cgroup configuration empty") 380 } 381 382 // Prevent a race between Wait/ForceStop 383 e.l.Lock() 384 defer e.l.Unlock() 385 386 manager := e.getCgroupManager(e.groups) 387 pids, err := manager.GetPids() 388 if err != nil { 389 return fmt.Errorf("Failed to get pids in the cgroup %v: %v", e.groups.Name, err) 390 } 391 392 errs := new(multierror.Error) 393 for _, pid := range pids { 394 process, err := os.FindProcess(pid) 395 if err != nil { 396 multierror.Append(errs, fmt.Errorf("Failed to find Pid %v: %v", pid, err)) 397 continue 398 } 399 400 if err := process.Kill(); err != nil && err.Error() != "os: process already finished" { 401 multierror.Append(errs, fmt.Errorf("Failed to kill Pid %v: %v", pid, err)) 402 continue 403 } 404 } 405 406 // Remove the cgroup. 407 if err := manager.Destroy(); err != nil { 408 multierror.Append(errs, fmt.Errorf("Failed to delete the cgroup directories: %v", err)) 409 } 410 411 if len(errs.Errors) != 0 { 412 return fmt.Errorf("Failed to destroy cgroup: %v", errs) 413 } 414 415 return nil 416 } 417 418 // getCgroupManager returns the correct libcontainer cgroup manager. 419 func (e *LinuxExecutor) getCgroupManager(groups *cgroupConfig.Cgroup) cgroups.Manager { 420 var manager cgroups.Manager 421 manager = &cgroupFs.Manager{Cgroups: groups} 422 if systemd.UseSystemd() { 423 manager = &systemd.Manager{Cgroups: groups} 424 } 425 return manager 426 }