1// +build windows 2 3package lcow // import "github.com/docker/docker/daemon/graphdriver/lcow" 4 5import ( 6 "bytes" 7 "errors" 8 "fmt" 9 "io" 10 "strings" 11 "sync" 12 "time" 13 14 "github.com/Microsoft/hcsshim" 15 "github.com/Microsoft/opengcs/client" 16 "github.com/sirupsen/logrus" 17) 18 19// Code for all the service VM management for the LCOW graphdriver 20 21var errVMisTerminating = errors.New("service VM is shutting down") 22var errVMUnknown = errors.New("service vm id is unknown") 23var errVMStillHasReference = errors.New("Attemping to delete a VM that is still being used") 24 25// serviceVMMap is the struct representing the id -> service VM mapping. 26type serviceVMMap struct { 27 sync.Mutex 28 svms map[string]*serviceVMMapItem 29} 30 31// serviceVMMapItem is our internal structure representing an item in our 32// map of service VMs we are maintaining. 33type serviceVMMapItem struct { 34 svm *serviceVM // actual service vm object 35 refCount int // refcount for VM 36} 37 38// attachedVHD is for reference counting SCSI disks attached to a service VM, 39// and for a counter used to generate a short path name for the container path. 40type attachedVHD struct { 41 refCount int 42 attachCounter uint64 43} 44 45type serviceVM struct { 46 sync.Mutex // Serialises operations being performed in this service VM. 47 scratchAttached bool // Has a scratch been attached? 48 config *client.Config // Represents the service VM item. 49 50 // Indicates that the vm is started 51 startStatus chan interface{} 52 startError error 53 54 // Indicates that the vm is stopped 55 stopStatus chan interface{} 56 stopError error 57 58 attachCounter uint64 // Increasing counter for each add 59 attachedVHDs map[string]*attachedVHD // Map ref counting all the VHDS we've hot-added/hot-removed. 60 unionMounts map[string]int // Map ref counting all the union filesystems we mounted. 61} 62 63// add will add an id to the service vm map. There are three cases: 64// - entry doesn't exist: 65// - add id to map and return a new vm that the caller can manually configure+start 66// - entry does exist 67// - return vm in map and increment ref count 68// - entry does exist but the ref count is 0 69// - return the svm and errVMisTerminating. Caller can call svm.getStopError() to wait for stop 70func (svmMap *serviceVMMap) add(id string) (svm *serviceVM, alreadyExists bool, err error) { 71 svmMap.Lock() 72 defer svmMap.Unlock() 73 if svm, ok := svmMap.svms[id]; ok { 74 if svm.refCount == 0 { 75 return svm.svm, true, errVMisTerminating 76 } 77 svm.refCount++ 78 return svm.svm, true, nil 79 } 80 81 // Doesn't exist, so create an empty svm to put into map and return 82 newSVM := &serviceVM{ 83 startStatus: make(chan interface{}), 84 stopStatus: make(chan interface{}), 85 attachedVHDs: make(map[string]*attachedVHD), 86 unionMounts: make(map[string]int), 87 config: &client.Config{}, 88 } 89 svmMap.svms[id] = &serviceVMMapItem{ 90 svm: newSVM, 91 refCount: 1, 92 } 93 return newSVM, false, nil 94} 95 96// get will get the service vm from the map. There are three cases: 97// - entry doesn't exist: 98// - return errVMUnknown 99// - entry does exist 100// - return vm with no error 101// - entry does exist but the ref count is 0 102// - return the svm and errVMisTerminating. Caller can call svm.getStopError() to wait for stop 103func (svmMap *serviceVMMap) get(id string) (*serviceVM, error) { 104 svmMap.Lock() 105 defer svmMap.Unlock() 106 svm, ok := svmMap.svms[id] 107 if !ok { 108 return nil, errVMUnknown 109 } 110 if svm.refCount == 0 { 111 return svm.svm, errVMisTerminating 112 } 113 return svm.svm, nil 114} 115 116// decrementRefCount decrements the ref count of the given ID from the map. There are four cases: 117// - entry doesn't exist: 118// - return errVMUnknown 119// - entry does exist but the ref count is 0 120// - return the svm and errVMisTerminating. Caller can call svm.getStopError() to wait for stop 121// - entry does exist but ref count is 1 122// - return vm and set lastRef to true. The caller can then stop the vm, delete the id from this map 123// - and execute svm.signalStopFinished to signal the threads that the svm has been terminated. 124// - entry does exist and ref count > 1 125// - just reduce ref count and return svm 126func (svmMap *serviceVMMap) decrementRefCount(id string) (_ *serviceVM, lastRef bool, _ error) { 127 svmMap.Lock() 128 defer svmMap.Unlock() 129 130 svm, ok := svmMap.svms[id] 131 if !ok { 132 return nil, false, errVMUnknown 133 } 134 if svm.refCount == 0 { 135 return svm.svm, false, errVMisTerminating 136 } 137 svm.refCount-- 138 return svm.svm, svm.refCount == 0, nil 139} 140 141// setRefCountZero works the same way as decrementRefCount, but sets ref count to 0 instead of decrementing it. 142func (svmMap *serviceVMMap) setRefCountZero(id string) (*serviceVM, error) { 143 svmMap.Lock() 144 defer svmMap.Unlock() 145 146 svm, ok := svmMap.svms[id] 147 if !ok { 148 return nil, errVMUnknown 149 } 150 if svm.refCount == 0 { 151 return svm.svm, errVMisTerminating 152 } 153 svm.refCount = 0 154 return svm.svm, nil 155} 156 157// deleteID deletes the given ID from the map. If the refcount is not 0 or the 158// VM does not exist, then this function returns an error. 159func (svmMap *serviceVMMap) deleteID(id string) error { 160 svmMap.Lock() 161 defer svmMap.Unlock() 162 svm, ok := svmMap.svms[id] 163 if !ok { 164 return errVMUnknown 165 } 166 if svm.refCount != 0 { 167 return errVMStillHasReference 168 } 169 delete(svmMap.svms, id) 170 return nil 171} 172 173func (svm *serviceVM) signalStartFinished(err error) { 174 svm.Lock() 175 svm.startError = err 176 svm.Unlock() 177 close(svm.startStatus) 178} 179 180func (svm *serviceVM) getStartError() error { 181 <-svm.startStatus 182 svm.Lock() 183 defer svm.Unlock() 184 return svm.startError 185} 186 187func (svm *serviceVM) signalStopFinished(err error) { 188 svm.Lock() 189 svm.stopError = err 190 svm.Unlock() 191 close(svm.stopStatus) 192} 193 194func (svm *serviceVM) getStopError() error { 195 <-svm.stopStatus 196 svm.Lock() 197 defer svm.Unlock() 198 return svm.stopError 199} 200 201// hotAddVHDs waits for the service vm to start and then attaches the vhds. 202func (svm *serviceVM) hotAddVHDs(mvds ...hcsshim.MappedVirtualDisk) error { 203 if err := svm.getStartError(); err != nil { 204 return err 205 } 206 return svm.hotAddVHDsAtStart(mvds...) 207} 208 209// hotAddVHDsAtStart works the same way as hotAddVHDs but does not wait for the VM to start. 210func (svm *serviceVM) hotAddVHDsAtStart(mvds ...hcsshim.MappedVirtualDisk) error { 211 svm.Lock() 212 defer svm.Unlock() 213 for i, mvd := range mvds { 214 if _, ok := svm.attachedVHDs[mvd.HostPath]; ok { 215 svm.attachedVHDs[mvd.HostPath].refCount++ 216 logrus.Debugf("lcowdriver: UVM %s: %s already present, refCount now %d", svm.config.Name, mvd.HostPath, svm.attachedVHDs[mvd.HostPath].refCount) 217 continue 218 } 219 220 svm.attachCounter++ 221 shortContainerPath := remapLongToShortContainerPath(mvd.ContainerPath, svm.attachCounter, svm.config.Name) 222 if err := svm.config.HotAddVhd(mvd.HostPath, shortContainerPath, mvd.ReadOnly, !mvd.AttachOnly); err != nil { 223 svm.hotRemoveVHDsNoLock(mvds[:i]...) 224 return err 225 } 226 svm.attachedVHDs[mvd.HostPath] = &attachedVHD{refCount: 1, attachCounter: svm.attachCounter} 227 } 228 return nil 229} 230 231// hotRemoveVHDs waits for the service vm to start and then removes the vhds. 232// The service VM must not be locked when calling this function. 233func (svm *serviceVM) hotRemoveVHDs(mvds ...hcsshim.MappedVirtualDisk) error { 234 if err := svm.getStartError(); err != nil { 235 return err 236 } 237 svm.Lock() 238 defer svm.Unlock() 239 return svm.hotRemoveVHDsNoLock(mvds...) 240} 241 242// hotRemoveVHDsNoLock removes VHDs from a service VM. When calling this function, 243// the contract is the service VM lock must be held. 244func (svm *serviceVM) hotRemoveVHDsNoLock(mvds ...hcsshim.MappedVirtualDisk) error { 245 var retErr error 246 for _, mvd := range mvds { 247 if _, ok := svm.attachedVHDs[mvd.HostPath]; !ok { 248 // We continue instead of returning an error if we try to hot remove a non-existent VHD. 249 // This is because one of the callers of the function is graphdriver.Put(). Since graphdriver.Get() 250 // defers the VM start to the first operation, it's possible that nothing have been hot-added 251 // when Put() is called. To avoid Put returning an error in that case, we simply continue if we 252 // don't find the vhd attached. 253 logrus.Debugf("lcowdriver: UVM %s: %s is not attached, not doing anything", svm.config.Name, mvd.HostPath) 254 continue 255 } 256 257 if svm.attachedVHDs[mvd.HostPath].refCount > 1 { 258 svm.attachedVHDs[mvd.HostPath].refCount-- 259 logrus.Debugf("lcowdriver: UVM %s: %s refCount dropped to %d. not removing from UVM", svm.config.Name, mvd.HostPath, svm.attachedVHDs[mvd.HostPath].refCount) 260 continue 261 } 262 263 // last reference to VHD, so remove from VM and map 264 if err := svm.config.HotRemoveVhd(mvd.HostPath); err == nil { 265 delete(svm.attachedVHDs, mvd.HostPath) 266 } else { 267 // Take note of the error, but still continue to remove the other VHDs 268 logrus.Warnf("Failed to hot remove %s: %s", mvd.HostPath, err) 269 if retErr == nil { 270 retErr = err 271 } 272 } 273 } 274 return retErr 275} 276 277func (svm *serviceVM) createExt4VHDX(destFile string, sizeGB uint32, cacheFile string) error { 278 if err := svm.getStartError(); err != nil { 279 return err 280 } 281 282 svm.Lock() 283 defer svm.Unlock() 284 return svm.config.CreateExt4Vhdx(destFile, sizeGB, cacheFile) 285} 286 287// getShortContainerPath looks up where a SCSI disk was actually mounted 288// in a service VM when we remapped a long path name to a short name. 289func (svm *serviceVM) getShortContainerPath(mvd *hcsshim.MappedVirtualDisk) string { 290 if mvd.ContainerPath == "" { 291 return "" 292 } 293 avhd, ok := svm.attachedVHDs[mvd.HostPath] 294 if !ok { 295 return "" 296 } 297 return fmt.Sprintf("/tmp/d%d", avhd.attachCounter) 298} 299 300func (svm *serviceVM) createUnionMount(mountName string, mvds ...hcsshim.MappedVirtualDisk) (err error) { 301 if len(mvds) == 0 { 302 return fmt.Errorf("createUnionMount: error must have at least 1 layer") 303 } 304 305 if err = svm.getStartError(); err != nil { 306 return err 307 } 308 309 svm.Lock() 310 defer svm.Unlock() 311 if _, ok := svm.unionMounts[mountName]; ok { 312 svm.unionMounts[mountName]++ 313 return nil 314 } 315 316 var lowerLayers []string 317 if mvds[0].ReadOnly { 318 lowerLayers = append(lowerLayers, svm.getShortContainerPath(&mvds[0])) 319 } 320 321 for i := 1; i < len(mvds); i++ { 322 lowerLayers = append(lowerLayers, svm.getShortContainerPath(&mvds[i])) 323 } 324 325 logrus.Debugf("Doing the overlay mount with union directory=%s", mountName) 326 if err = svm.runProcess(fmt.Sprintf("mkdir -p %s", mountName), nil, nil, nil); err != nil { 327 return err 328 } 329 330 var cmd string 331 if len(mvds) == 1 { 332 // `FROM SCRATCH` case and the only layer. No overlay required. 333 cmd = fmt.Sprintf("mount %s %s", svm.getShortContainerPath(&mvds[0]), mountName) 334 } else if mvds[0].ReadOnly { 335 // Readonly overlay 336 cmd = fmt.Sprintf("mount -t overlay overlay -olowerdir=%s %s", 337 strings.Join(lowerLayers, ","), 338 mountName) 339 } else { 340 upper := fmt.Sprintf("%s/upper", svm.getShortContainerPath(&mvds[0])) 341 work := fmt.Sprintf("%s/work", svm.getShortContainerPath(&mvds[0])) 342 343 if err = svm.runProcess(fmt.Sprintf("mkdir -p %s %s", upper, work), nil, nil, nil); err != nil { 344 return err 345 } 346 347 cmd = fmt.Sprintf("mount -t overlay overlay -olowerdir=%s,upperdir=%s,workdir=%s %s", 348 strings.Join(lowerLayers, ":"), 349 upper, 350 work, 351 mountName) 352 } 353 354 logrus.Debugf("createUnionMount: Executing mount=%s", cmd) 355 if err = svm.runProcess(cmd, nil, nil, nil); err != nil { 356 return err 357 } 358 359 svm.unionMounts[mountName] = 1 360 return nil 361} 362 363func (svm *serviceVM) deleteUnionMount(mountName string, disks ...hcsshim.MappedVirtualDisk) error { 364 if err := svm.getStartError(); err != nil { 365 return err 366 } 367 368 svm.Lock() 369 defer svm.Unlock() 370 if _, ok := svm.unionMounts[mountName]; !ok { 371 return nil 372 } 373 374 if svm.unionMounts[mountName] > 1 { 375 svm.unionMounts[mountName]-- 376 return nil 377 } 378 379 logrus.Debugf("Removing union mount %s", mountName) 380 if err := svm.runProcess(fmt.Sprintf("umount %s", mountName), nil, nil, nil); err != nil { 381 return err 382 } 383 384 delete(svm.unionMounts, mountName) 385 return nil 386} 387 388func (svm *serviceVM) runProcess(command string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error { 389 var process hcsshim.Process 390 var err error 391 errOut := &bytes.Buffer{} 392 393 if stderr != nil { 394 process, err = svm.config.RunProcess(command, stdin, stdout, stderr) 395 } else { 396 process, err = svm.config.RunProcess(command, stdin, stdout, errOut) 397 } 398 if err != nil { 399 return err 400 } 401 defer process.Close() 402 403 process.WaitTimeout(time.Duration(int(time.Second) * svm.config.UvmTimeoutSeconds)) 404 exitCode, err := process.ExitCode() 405 if err != nil { 406 return err 407 } 408 409 if exitCode != 0 { 410 // If the caller isn't explicitly capturing stderr output, then capture it here instead. 411 e := fmt.Sprintf("svm.runProcess: command %s failed with exit code %d", command, exitCode) 412 if stderr == nil { 413 e = fmt.Sprintf("%s. (%s)", e, errOut.String()) 414 } 415 return fmt.Errorf(e) 416 } 417 return nil 418} 419