github.com/hhrutter/nomad@v0.6.0-rc2.0.20170723054333-80c4b03f0705/client/driver/executor/executor_linux.go (about) 1 package executor 2 3 import ( 4 "fmt" 5 "os" 6 "os/user" 7 "path/filepath" 8 "strconv" 9 "strings" 10 "syscall" 11 "time" 12 13 "github.com/hashicorp/go-multierror" 14 "github.com/mitchellh/go-ps" 15 "github.com/opencontainers/runc/libcontainer/cgroups" 16 cgroupFs "github.com/opencontainers/runc/libcontainer/cgroups/fs" 17 cgroupConfig "github.com/opencontainers/runc/libcontainer/configs" 18 19 "github.com/hashicorp/nomad/client/stats" 20 cstructs "github.com/hashicorp/nomad/client/structs" 21 "github.com/hashicorp/nomad/nomad/structs" 22 ) 23 24 var ( 25 // The statistics the executor exposes when using cgroups 26 ExecutorCgroupMeasuredMemStats = []string{"RSS", "Cache", "Swap", "Max Usage", "Kernel Usage", "Kernel Max Usage"} 27 ExecutorCgroupMeasuredCpuStats = []string{"System Mode", "User Mode", "Throttled Periods", "Throttled Time", "Percent"} 28 ) 29 30 // configureIsolation configures chroot and creates cgroups 31 func (e *UniversalExecutor) configureIsolation() error { 32 if e.command.FSIsolation { 33 if err := e.configureChroot(); err != nil { 34 return err 35 } 36 } 37 38 if e.command.ResourceLimits { 39 if err := e.configureCgroups(e.ctx.Task.Resources); err != nil { 40 return fmt.Errorf("error creating cgroups: %v", err) 41 } 42 } 43 return nil 44 } 45 46 // applyLimits puts a process in a pre-configured cgroup 47 func (e *UniversalExecutor) applyLimits(pid int) error { 48 if !e.command.ResourceLimits { 49 return nil 50 } 51 52 // Entering the process in the cgroup 53 manager := getCgroupManager(e.resConCtx.groups, nil) 54 if err := manager.Apply(pid); err != nil { 55 e.logger.Printf("[ERR] executor: error applying pid to cgroup: %v", err) 56 return err 57 } 58 e.resConCtx.cgPaths = manager.GetPaths() 59 cgConfig := cgroupConfig.Config{Cgroups: e.resConCtx.groups} 60 if err := manager.Set(&cgConfig); err != nil { 61 e.logger.Printf("[ERR] executor: error setting cgroup config: %v", err) 62 if er := DestroyCgroup(e.resConCtx.groups, e.resConCtx.cgPaths, os.Getpid()); er != nil { 63 e.logger.Printf("[ERR] executor: error destroying cgroup: %v", er) 64 } 65 return err 66 } 67 return nil 68 } 69 70 // configureCgroups converts a Nomad Resources specification into the equivalent 71 // cgroup configuration. It returns an error if the resources are invalid. 72 func (e *UniversalExecutor) configureCgroups(resources *structs.Resources) error { 73 e.resConCtx.groups = &cgroupConfig.Cgroup{} 74 e.resConCtx.groups.Resources = &cgroupConfig.Resources{} 75 cgroupName := structs.GenerateUUID() 76 e.resConCtx.groups.Path = filepath.Join("/nomad", cgroupName) 77 78 // TODO: verify this is needed for things like network access 79 e.resConCtx.groups.Resources.AllowAllDevices = true 80 81 if resources.MemoryMB > 0 { 82 // Total amount of memory allowed to consume 83 e.resConCtx.groups.Resources.Memory = int64(resources.MemoryMB * 1024 * 1024) 84 // Disable swap to avoid issues on the machine 85 e.resConCtx.groups.Resources.MemorySwap = int64(-1) 86 } 87 88 if resources.CPU < 2 { 89 return fmt.Errorf("resources.CPU must be equal to or greater than 2: %v", resources.CPU) 90 } 91 92 // Set the relative CPU shares for this cgroup. 93 e.resConCtx.groups.Resources.CpuShares = int64(resources.CPU) 94 95 if resources.IOPS != 0 { 96 // Validate it is in an acceptable range. 97 if resources.IOPS < 10 || resources.IOPS > 1000 { 98 return fmt.Errorf("resources.IOPS must be between 10 and 1000: %d", resources.IOPS) 99 } 100 101 e.resConCtx.groups.Resources.BlkioWeight = uint16(resources.IOPS) 102 } 103 104 return nil 105 } 106 107 // Stats reports the resource utilization of the cgroup. If there is no resource 108 // isolation we aggregate the resource utilization of all the pids launched by 109 // the executor. 110 func (e *UniversalExecutor) Stats() (*cstructs.TaskResourceUsage, error) { 111 if !e.command.ResourceLimits { 112 pidStats, err := e.pidStats() 113 if err != nil { 114 return nil, err 115 } 116 return e.aggregatedResourceUsage(pidStats), nil 117 } 118 ts := time.Now() 119 manager := getCgroupManager(e.resConCtx.groups, e.resConCtx.cgPaths) 120 stats, err := manager.GetStats() 121 if err != nil { 122 return nil, err 123 } 124 125 // Memory Related Stats 126 swap := stats.MemoryStats.SwapUsage 127 maxUsage := stats.MemoryStats.Usage.MaxUsage 128 rss := stats.MemoryStats.Stats["rss"] 129 cache := stats.MemoryStats.Stats["cache"] 130 ms := &cstructs.MemoryStats{ 131 RSS: rss, 132 Cache: cache, 133 Swap: swap.Usage, 134 MaxUsage: maxUsage, 135 KernelUsage: stats.MemoryStats.KernelUsage.Usage, 136 KernelMaxUsage: stats.MemoryStats.KernelUsage.MaxUsage, 137 Measured: ExecutorCgroupMeasuredMemStats, 138 } 139 140 // CPU Related Stats 141 totalProcessCPUUsage := float64(stats.CpuStats.CpuUsage.TotalUsage) 142 userModeTime := float64(stats.CpuStats.CpuUsage.UsageInUsermode) 143 kernelModeTime := float64(stats.CpuStats.CpuUsage.UsageInKernelmode) 144 145 totalPercent := e.totalCpuStats.Percent(totalProcessCPUUsage) 146 cs := &cstructs.CpuStats{ 147 SystemMode: e.systemCpuStats.Percent(kernelModeTime), 148 UserMode: e.userCpuStats.Percent(userModeTime), 149 Percent: totalPercent, 150 ThrottledPeriods: stats.CpuStats.ThrottlingData.ThrottledPeriods, 151 ThrottledTime: stats.CpuStats.ThrottlingData.ThrottledTime, 152 TotalTicks: e.systemCpuStats.TicksConsumed(totalPercent), 153 Measured: ExecutorCgroupMeasuredCpuStats, 154 } 155 taskResUsage := cstructs.TaskResourceUsage{ 156 ResourceUsage: &cstructs.ResourceUsage{ 157 MemoryStats: ms, 158 CpuStats: cs, 159 }, 160 Timestamp: ts.UTC().UnixNano(), 161 } 162 if pidStats, err := e.pidStats(); err == nil { 163 taskResUsage.Pids = pidStats 164 } 165 return &taskResUsage, nil 166 } 167 168 // runAs takes a user id as a string and looks up the user, and sets the command 169 // to execute as that user. 170 func (e *UniversalExecutor) runAs(userid string) error { 171 u, err := user.Lookup(userid) 172 if err != nil { 173 return fmt.Errorf("Failed to identify user %v: %v", userid, err) 174 } 175 176 // Get the groups the user is a part of 177 gidStrings, err := u.GroupIds() 178 if err != nil { 179 return fmt.Errorf("Unable to lookup user's group membership: %v", err) 180 } 181 182 gids := make([]uint32, len(gidStrings)) 183 for _, gidString := range gidStrings { 184 u, err := strconv.Atoi(gidString) 185 if err != nil { 186 return fmt.Errorf("Unable to convert user's group to int %s: %v", gidString, err) 187 } 188 189 gids = append(gids, uint32(u)) 190 } 191 192 // Convert the uid and gid 193 uid, err := strconv.ParseUint(u.Uid, 10, 32) 194 if err != nil { 195 return fmt.Errorf("Unable to convert userid to uint32: %s", err) 196 } 197 gid, err := strconv.ParseUint(u.Gid, 10, 32) 198 if err != nil { 199 return fmt.Errorf("Unable to convert groupid to uint32: %s", err) 200 } 201 202 // Set the command to run as that user and group. 203 if e.cmd.SysProcAttr == nil { 204 e.cmd.SysProcAttr = &syscall.SysProcAttr{} 205 } 206 if e.cmd.SysProcAttr.Credential == nil { 207 e.cmd.SysProcAttr.Credential = &syscall.Credential{} 208 } 209 e.cmd.SysProcAttr.Credential.Uid = uint32(uid) 210 e.cmd.SysProcAttr.Credential.Gid = uint32(gid) 211 e.cmd.SysProcAttr.Credential.Groups = gids 212 213 e.logger.Printf("[DEBUG] executor: running as user:group %d:%d with group membership in %v", uid, gid, gids) 214 215 return nil 216 } 217 218 // configureChroot configures a chroot 219 func (e *UniversalExecutor) configureChroot() error { 220 if e.cmd.SysProcAttr == nil { 221 e.cmd.SysProcAttr = &syscall.SysProcAttr{} 222 } 223 e.cmd.SysProcAttr.Chroot = e.ctx.TaskDir 224 e.cmd.Dir = "/" 225 226 e.fsIsolationEnforced = true 227 return nil 228 } 229 230 // getAllPids returns the pids of all the processes spun up by the executor. We 231 // use the libcontainer apis to get the pids when the user is using cgroup 232 // isolation and we scan the entire process table if the user is not using any 233 // isolation 234 func (e *UniversalExecutor) getAllPids() (map[int]*nomadPid, error) { 235 if e.command.ResourceLimits { 236 manager := getCgroupManager(e.resConCtx.groups, e.resConCtx.cgPaths) 237 pids, err := manager.GetAllPids() 238 if err != nil { 239 return nil, err 240 } 241 np := make(map[int]*nomadPid, len(pids)) 242 for _, pid := range pids { 243 np[pid] = &nomadPid{ 244 pid: pid, 245 cpuStatsTotal: stats.NewCpuStats(), 246 cpuStatsSys: stats.NewCpuStats(), 247 cpuStatsUser: stats.NewCpuStats(), 248 } 249 } 250 return np, nil 251 } 252 allProcesses, err := ps.Processes() 253 if err != nil { 254 return nil, err 255 } 256 return e.scanPids(os.Getpid(), allProcesses) 257 } 258 259 // destroyCgroup kills all processes in the cgroup and removes the cgroup 260 // configuration from the host. This function is idempotent. 261 func DestroyCgroup(groups *cgroupConfig.Cgroup, cgPaths map[string]string, executorPid int) error { 262 mErrs := new(multierror.Error) 263 if groups == nil { 264 return fmt.Errorf("Can't destroy: cgroup configuration empty") 265 } 266 267 // Move the executor into the global cgroup so that the task specific 268 // cgroup can be destroyed. 269 nilGroup := &cgroupConfig.Cgroup{} 270 nilGroup.Path = "/" 271 nilGroup.Resources = groups.Resources 272 nilManager := getCgroupManager(nilGroup, nil) 273 err := nilManager.Apply(executorPid) 274 if err != nil && !strings.Contains(err.Error(), "no such process") { 275 return fmt.Errorf("failed to remove executor pid %d: %v", executorPid, err) 276 } 277 278 // Freeze the Cgroup so that it can not continue to fork/exec. 279 manager := getCgroupManager(groups, cgPaths) 280 err = manager.Freeze(cgroupConfig.Frozen) 281 if err != nil && !strings.Contains(err.Error(), "no such file or directory") { 282 return fmt.Errorf("failed to freeze cgroup: %v", err) 283 } 284 285 var procs []*os.Process 286 pids, err := manager.GetAllPids() 287 if err != nil { 288 multierror.Append(mErrs, fmt.Errorf("error getting pids: %v", err)) 289 290 // Unfreeze the cgroup. 291 err = manager.Freeze(cgroupConfig.Thawed) 292 if err != nil && !strings.Contains(err.Error(), "no such file or directory") { 293 multierror.Append(mErrs, fmt.Errorf("failed to unfreeze cgroup: %v", err)) 294 } 295 return mErrs.ErrorOrNil() 296 } 297 298 // Kill the processes in the cgroup 299 for _, pid := range pids { 300 proc, err := os.FindProcess(pid) 301 if err != nil { 302 multierror.Append(mErrs, fmt.Errorf("error finding process %v: %v", pid, err)) 303 continue 304 } 305 306 procs = append(procs, proc) 307 if e := proc.Kill(); e != nil { 308 multierror.Append(mErrs, fmt.Errorf("error killing process %v: %v", pid, e)) 309 } 310 } 311 312 // Unfreeze the cgroug so we can wait. 313 err = manager.Freeze(cgroupConfig.Thawed) 314 if err != nil && !strings.Contains(err.Error(), "no such file or directory") { 315 multierror.Append(mErrs, fmt.Errorf("failed to unfreeze cgroup: %v", err)) 316 } 317 318 // Wait on the killed processes to ensure they are cleaned up. 319 for _, proc := range procs { 320 // Don't capture the error because we expect this to fail for 321 // processes we didn't fork. 322 proc.Wait() 323 } 324 325 // Remove the cgroup. 326 if err := manager.Destroy(); err != nil { 327 multierror.Append(mErrs, fmt.Errorf("failed to delete the cgroup directories: %v", err)) 328 } 329 return mErrs.ErrorOrNil() 330 } 331 332 // getCgroupManager returns the correct libcontainer cgroup manager. 333 func getCgroupManager(groups *cgroupConfig.Cgroup, paths map[string]string) cgroups.Manager { 334 return &cgroupFs.Manager{Cgroups: groups, Paths: paths} 335 }