github.com/cloud-foundations/dominator@v0.0.0-20221004181915-6e4fee580046/imageserver/scanner/imdb.go (about) 1 package scanner 2 3 import ( 4 "bufio" 5 "encoding/gob" 6 "errors" 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "syscall" 12 "time" 13 14 "github.com/Cloud-Foundations/Dominator/lib/fsutil" 15 "github.com/Cloud-Foundations/Dominator/lib/hash" 16 "github.com/Cloud-Foundations/Dominator/lib/image" 17 "github.com/Cloud-Foundations/Dominator/lib/log" 18 "github.com/Cloud-Foundations/Dominator/lib/srpc" 19 ) 20 21 const ( 22 dirPerms = syscall.S_IRWXU | syscall.S_IRGRP | syscall.S_IXGRP | 23 syscall.S_IROTH | syscall.S_IXOTH 24 filePerms = syscall.S_IRUSR | syscall.S_IWUSR | syscall.S_IRGRP | 25 syscall.S_IROTH 26 ) 27 28 var ( 29 errNoAccess = errors.New("no access to image") 30 errNoAuthInfo = errors.New("no authentication information") 31 ) 32 33 func (imdb *ImageDataBase) addImage(image *image.Image, name string, 34 authInfo *srpc.AuthInformation) error { 35 if err := image.Verify(); err != nil { 36 return err 37 } 38 if imageIsExpired(image) { 39 imdb.logger.Printf("Ignoring already expired image: %s\n", name) 40 return nil 41 } 42 imdb.deduperLock.Lock() 43 image.ReplaceStrings(imdb.deduper.DeDuplicate) 44 imdb.deduperLock.Unlock() 45 imdb.Lock() 46 defer imdb.Unlock() 47 if _, ok := imdb.imageMap[name]; ok { 48 return errors.New("image: " + name + " already exists") 49 } else { 50 if err := imdb.checkPermissions(name, authInfo); err != nil { 51 return err 52 } 53 filename := filepath.Join(imdb.baseDir, name) 54 flags := os.O_CREATE | os.O_RDWR 55 if imdb.replicationMaster != "" { 56 flags |= os.O_EXCL 57 } else { 58 flags |= os.O_TRUNC 59 } 60 file, err := os.OpenFile(filename, flags, filePerms) 61 if err != nil { 62 if os.IsExist(err) { 63 return errors.New("cannot add previously deleted image: " + 64 name) 65 } 66 return err 67 } 68 defer file.Close() 69 w := bufio.NewWriter(file) 70 defer w.Flush() 71 writer := fsutil.NewChecksumWriter(w) 72 defer writer.WriteChecksum() 73 encoder := gob.NewEncoder(writer) 74 if err := encoder.Encode(image); err != nil { 75 os.Remove(filename) 76 return err 77 } 78 if err := w.Flush(); err != nil { 79 os.Remove(filename) 80 return err 81 } 82 imdb.scheduleExpiration(image, name) 83 imdb.imageMap[name] = image 84 imdb.addNotifiers.sendPlain(name, "add", imdb.logger) 85 imdb.removeFromUnreferencedObjectsListAndSave(image) 86 return nil 87 } 88 } 89 90 func (imdb *ImageDataBase) changeImageExpiration(name string, 91 expiresAt time.Time, authInfo *srpc.AuthInformation) (bool, error) { 92 imdb.Lock() 93 defer imdb.Unlock() 94 if img, ok := imdb.imageMap[name]; !ok { 95 return false, errors.New("image not found") 96 } else if err := imdb.checkPermissions(name, authInfo); err != nil { 97 return false, err 98 } else if img.ExpiresAt.IsZero() { 99 return false, errors.New("image does not expire") 100 } else if expiresAt.IsZero() { 101 if err := imdb.writeNewExpiration(name, img, expiresAt); err != nil { 102 return false, err 103 } 104 img.ExpiresAt = expiresAt 105 imdb.addNotifiers.sendPlain(name, "add", imdb.logger) 106 return true, nil 107 } else if expiresAt.Before(img.ExpiresAt) { 108 return false, errors.New("cannot shorten expiration time") 109 } else if expiresAt.After(img.ExpiresAt) { 110 if err := imdb.writeNewExpiration(name, img, expiresAt); err != nil { 111 return false, err 112 } 113 img.ExpiresAt = expiresAt 114 imdb.addNotifiers.sendPlain(name, "add", imdb.logger) 115 return true, nil 116 } else { 117 return false, nil 118 } 119 } 120 121 // This must be called with the lock held. 122 func (imdb *ImageDataBase) checkChown(dirname, ownerGroup string, 123 authInfo *srpc.AuthInformation) error { 124 if authInfo == nil { 125 return errNoAuthInfo 126 } 127 if authInfo.HaveMethodAccess { 128 return nil 129 } 130 // If owner of parent, any group can be set. 131 parentDirname := filepath.Dir(dirname) 132 if directoryMetadata, ok := imdb.directoryMap[parentDirname]; ok { 133 if directoryMetadata.OwnerGroup != "" { 134 if _, ok := authInfo.GroupList[directoryMetadata.OwnerGroup]; ok { 135 return nil 136 } 137 } 138 } 139 if _, ok := authInfo.GroupList[ownerGroup]; !ok { 140 return fmt.Errorf("no membership of %s group", ownerGroup) 141 } 142 if directoryMetadata, ok := imdb.directoryMap[dirname]; !ok { 143 return fmt.Errorf("no metadata for: \"%s\"", dirname) 144 } else if directoryMetadata.OwnerGroup != "" { 145 if _, ok := authInfo.GroupList[directoryMetadata.OwnerGroup]; ok { 146 return nil 147 } 148 } 149 return errNoAccess 150 } 151 152 func (imdb *ImageDataBase) checkDirectory(name string) bool { 153 imdb.RLock() 154 defer imdb.RUnlock() 155 _, ok := imdb.directoryMap[name] 156 return ok 157 } 158 159 func (imdb *ImageDataBase) checkImage(name string) bool { 160 imdb.RLock() 161 defer imdb.RUnlock() 162 _, ok := imdb.imageMap[name] 163 return ok 164 } 165 166 // This must be called with the lock held. 167 func (imdb *ImageDataBase) checkPermissions(imageName string, 168 authInfo *srpc.AuthInformation) error { 169 if authInfo == nil { 170 return errNoAuthInfo 171 } 172 if authInfo.HaveMethodAccess { 173 return nil 174 } 175 dirname := filepath.Dir(imageName) 176 if directoryMetadata, ok := imdb.directoryMap[dirname]; !ok { 177 return fmt.Errorf("no metadata for: \"%s\"", dirname) 178 } else if directoryMetadata.OwnerGroup != "" { 179 if _, ok := authInfo.GroupList[directoryMetadata.OwnerGroup]; ok { 180 return nil 181 } 182 } 183 return errNoAccess 184 } 185 186 func (imdb *ImageDataBase) chownDirectory(dirname, ownerGroup string, 187 authInfo *srpc.AuthInformation) error { 188 dirname = filepath.Clean(dirname) 189 imdb.Lock() 190 defer imdb.Unlock() 191 directoryMetadata, ok := imdb.directoryMap[dirname] 192 if !ok { 193 return fmt.Errorf("no metadata for: \"%s\"", dirname) 194 } 195 if err := imdb.checkChown(dirname, ownerGroup, authInfo); err != nil { 196 return err 197 } 198 directoryMetadata.OwnerGroup = ownerGroup 199 return imdb.updateDirectoryMetadata( 200 image.Directory{Name: dirname, Metadata: directoryMetadata}) 201 } 202 203 // This must be called with the lock held. 204 func (imdb *ImageDataBase) updateDirectoryMetadata( 205 directory image.Directory) error { 206 oldDirectoryMetadata, ok := imdb.directoryMap[directory.Name] 207 if ok && directory.Metadata == oldDirectoryMetadata { 208 return nil 209 } 210 if err := imdb.updateDirectoryMetadataFile(directory); err != nil { 211 return err 212 } 213 imdb.directoryMap[directory.Name] = directory.Metadata 214 imdb.mkdirNotifiers.sendMakeDirectory(directory, imdb.logger) 215 return nil 216 } 217 218 func (imdb *ImageDataBase) updateDirectoryMetadataFile( 219 directory image.Directory) error { 220 filename := filepath.Join(imdb.baseDir, directory.Name, metadataFile) 221 _, ok := imdb.directoryMap[directory.Name] 222 if directory.Metadata == (image.DirectoryMetadata{}) { 223 if !ok { 224 return nil 225 } 226 return os.Remove(filename) 227 } 228 file, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR, filePerms) 229 if err != nil { 230 return err 231 } 232 if err := writeDirectoryMetadata(file, directory.Metadata); err != nil { 233 file.Close() 234 return err 235 } 236 return file.Close() 237 } 238 239 func writeDirectoryMetadata(file io.Writer, 240 directoryMetadata image.DirectoryMetadata) error { 241 w := bufio.NewWriter(file) 242 writer := fsutil.NewChecksumWriter(w) 243 if err := gob.NewEncoder(writer).Encode(directoryMetadata); err != nil { 244 return err 245 } 246 if err := writer.WriteChecksum(); err != nil { 247 return err 248 } 249 return w.Flush() 250 } 251 252 func (imdb *ImageDataBase) countDirectories() uint { 253 imdb.RLock() 254 defer imdb.RUnlock() 255 return uint(len(imdb.directoryMap)) 256 } 257 258 func (imdb *ImageDataBase) countImages() uint { 259 imdb.RLock() 260 defer imdb.RUnlock() 261 return uint(len(imdb.imageMap)) 262 } 263 264 func (imdb *ImageDataBase) deleteImage(name string, 265 authInfo *srpc.AuthInformation) error { 266 imdb.Lock() 267 defer imdb.Unlock() 268 if _, ok := imdb.imageMap[name]; ok { 269 if err := imdb.checkPermissions(name, authInfo); err != nil { 270 return err 271 } 272 filename := filepath.Join(imdb.baseDir, name) 273 if err := os.Truncate(filename, 0); err != nil { 274 return err 275 } 276 imdb.deleteImageAndUpdateUnreferencedObjectsList(name) 277 imdb.deleteNotifiers.sendPlain(name, "delete", imdb.logger) 278 return nil 279 } else { 280 return errors.New("image: " + name + " does not exist") 281 } 282 } 283 284 func (imdb *ImageDataBase) deleteImageAndUpdateUnreferencedObjectsList( 285 name string) { 286 img := imdb.imageMap[name] 287 if img == nil { // May be nil if expiring an already deleted image. 288 return 289 } 290 delete(imdb.imageMap, name) 291 imdb.rebuildDeDuper() 292 imdb.maybeAddToUnreferencedObjectsList(img.FileSystem) 293 } 294 295 func (imdb *ImageDataBase) deleteUnreferencedObjects(percentage uint8, 296 bytesThreshold uint64) error { 297 objects := imdb.listUnreferencedObjects() 298 objectsThreshold := uint64(percentage) * uint64(len(objects)) / 100 299 var objectsCount, bytesCount uint64 300 for hashVal, size := range objects { 301 if !(objectsCount < objectsThreshold || bytesCount < bytesThreshold) { 302 break 303 } 304 if err := imdb.objectServer.DeleteObject(hashVal); err != nil { 305 imdb.logger.Printf("Error cleaning up: %x: %s\n", hashVal, err) 306 return fmt.Errorf("error cleaning up: %x: %s\n", hashVal, err) 307 } 308 objectsCount++ 309 bytesCount += size 310 } 311 return nil 312 } 313 314 func (imdb *ImageDataBase) doWithPendingImage(image *image.Image, 315 doFunc func() error) error { 316 imdb.pendingImageLock.Lock() 317 defer imdb.pendingImageLock.Unlock() 318 imdb.Lock() 319 changed := imdb.removeFromUnreferencedObjectsList(image) 320 imdb.Unlock() 321 err := doFunc() 322 imdb.Lock() 323 defer imdb.Unlock() 324 for _, img := range imdb.imageMap { 325 if img == image { // image was added, save if change happened above. 326 if changed { 327 imdb.saveUnreferencedObjectsList(false) 328 } 329 return err 330 } 331 } 332 // image was not added: "delete" it by maybe adding to unreferenced list. 333 imdb.maybeAddToUnreferencedObjectsList(image.FileSystem) 334 return err 335 } 336 337 func (imdb *ImageDataBase) findLatestImage(dirname string, 338 ignoreExpiring bool) (string, error) { 339 imdb.RLock() 340 defer imdb.RUnlock() 341 if _, ok := imdb.directoryMap[dirname]; !ok { 342 return "", errors.New("unknown directory: " + dirname) 343 } 344 var previousCreateTime time.Time 345 var imageName string 346 for name, img := range imdb.imageMap { 347 if ignoreExpiring && !img.ExpiresAt.IsZero() { 348 continue 349 } 350 if filepath.Dir(name) != dirname { 351 continue 352 } 353 if img.CreatedOn.After(previousCreateTime) { 354 imageName = name 355 previousCreateTime = img.CreatedOn 356 } 357 } 358 return imageName, nil 359 } 360 361 func (imdb *ImageDataBase) getImage(name string) *image.Image { 362 imdb.RLock() 363 defer imdb.RUnlock() 364 return imdb.imageMap[name] 365 } 366 367 func (imdb *ImageDataBase) getUnreferencedObjectsStatistics() (uint64, uint64) { 368 imdb.maybeRegenerateUnreferencedObjectsList() 369 imdb.RLock() 370 defer imdb.RUnlock() 371 return uint64(len(imdb.unreferencedObjects.hashToEntry)), 372 imdb.unreferencedObjects.totalBytes 373 } 374 375 func (imdb *ImageDataBase) listDirectories() []image.Directory { 376 imdb.RLock() 377 defer imdb.RUnlock() 378 directories := make([]image.Directory, 0, len(imdb.directoryMap)) 379 for name, metadata := range imdb.directoryMap { 380 directories = append(directories, 381 image.Directory{Name: name, Metadata: metadata}) 382 } 383 return directories 384 } 385 386 func (imdb *ImageDataBase) listImages() []string { 387 imdb.RLock() 388 defer imdb.RUnlock() 389 names := make([]string, 0) 390 for name := range imdb.imageMap { 391 names = append(names, name) 392 } 393 return names 394 } 395 396 func (imdb *ImageDataBase) listUnreferencedObjects() map[hash.Hash]uint64 { 397 imdb.maybeRegenerateUnreferencedObjectsList() 398 imdb.RLock() 399 defer imdb.RUnlock() 400 return imdb.unreferencedObjects.list() 401 } 402 403 func (imdb *ImageDataBase) makeDirectory(directory image.Directory, 404 authInfo *srpc.AuthInformation, userRpc bool) error { 405 directory.Name = filepath.Clean(directory.Name) 406 pathname := filepath.Join(imdb.baseDir, directory.Name) 407 imdb.Lock() 408 defer imdb.Unlock() 409 oldDirectoryMetadata, ok := imdb.directoryMap[directory.Name] 410 if userRpc { 411 if authInfo == nil { 412 return errNoAuthInfo 413 } 414 if ok { 415 return fmt.Errorf("directory: %s already exists", directory.Name) 416 } 417 directory.Metadata = oldDirectoryMetadata 418 parentMetadata, ok := imdb.directoryMap[filepath.Dir(directory.Name)] 419 if !ok { 420 return fmt.Errorf("no metadata for: %s", 421 filepath.Dir(directory.Name)) 422 } 423 if !authInfo.HaveMethodAccess { 424 if parentMetadata.OwnerGroup == "" { 425 return errNoAccess 426 } 427 if _, ok := authInfo.GroupList[parentMetadata.OwnerGroup]; !ok { 428 return fmt.Errorf("no membership of %s group", 429 parentMetadata.OwnerGroup) 430 } 431 } 432 directory.Metadata.OwnerGroup = parentMetadata.OwnerGroup 433 } 434 if err := os.Mkdir(pathname, dirPerms); err != nil && !os.IsExist(err) { 435 return err 436 } 437 return imdb.updateDirectoryMetadata(directory) 438 } 439 440 // This must be called with the main lock held. 441 func (imdb *ImageDataBase) rebuildDeDuper() { 442 imdb.deduperLock.Lock() 443 defer imdb.deduperLock.Unlock() 444 startTime := time.Now() 445 imdb.deduper.Clear() 446 for _, image := range imdb.imageMap { 447 image.ReplaceStrings(imdb.deduper.DeDuplicate) 448 } 449 imdb.logger.Debugf(0, "Rebuilding de-duper state took %s\n", 450 time.Since(startTime)) 451 } 452 453 func (imdb *ImageDataBase) registerAddNotifier() <-chan string { 454 channel := make(chan string, 1) 455 imdb.Lock() 456 defer imdb.Unlock() 457 imdb.addNotifiers[channel] = channel 458 return channel 459 } 460 461 func (imdb *ImageDataBase) registerDeleteNotifier() <-chan string { 462 channel := make(chan string, 1) 463 imdb.Lock() 464 defer imdb.Unlock() 465 imdb.deleteNotifiers[channel] = channel 466 return channel 467 } 468 469 func (imdb *ImageDataBase) registerMakeDirectoryNotifier() <-chan image.Directory { 470 channel := make(chan image.Directory, 1) 471 imdb.Lock() 472 defer imdb.Unlock() 473 imdb.mkdirNotifiers[channel] = channel 474 return channel 475 } 476 477 func (imdb *ImageDataBase) unregisterAddNotifier(channel <-chan string) { 478 imdb.Lock() 479 defer imdb.Unlock() 480 delete(imdb.addNotifiers, channel) 481 } 482 483 func (imdb *ImageDataBase) unregisterDeleteNotifier(channel <-chan string) { 484 imdb.Lock() 485 defer imdb.Unlock() 486 delete(imdb.deleteNotifiers, channel) 487 } 488 489 func (imdb *ImageDataBase) unregisterMakeDirectoryNotifier( 490 channel <-chan image.Directory) { 491 imdb.Lock() 492 defer imdb.Unlock() 493 delete(imdb.mkdirNotifiers, channel) 494 } 495 496 // This must be called with the lock held. 497 func (imdb *ImageDataBase) writeNewExpiration(name string, 498 oldImage *image.Image, expiresAt time.Time) error { 499 img := *oldImage 500 img.ExpiresAt = expiresAt 501 filename := filepath.Join(imdb.baseDir, name) 502 tmpFilename := filename + "~" 503 file, err := os.OpenFile(tmpFilename, os.O_CREATE|os.O_RDWR|os.O_EXCL, 504 filePerms) 505 if err != nil { 506 return err 507 } 508 defer file.Close() 509 defer os.Remove(tmpFilename) 510 w := bufio.NewWriter(file) 511 defer w.Flush() 512 writer := fsutil.NewChecksumWriter(w) 513 encoder := gob.NewEncoder(writer) 514 if err := encoder.Encode(img); err != nil { 515 return err 516 } 517 if err := writer.WriteChecksum(); err != nil { 518 return err 519 } 520 if err := w.Flush(); err != nil { 521 return err 522 } 523 fsutil.FsyncFile(file) 524 return os.Rename(tmpFilename, filename) 525 } 526 527 func (n notifiers) sendPlain(name string, operation string, 528 logger log.Logger) { 529 if len(n) < 1 { 530 return 531 } else { 532 plural := "s" 533 if len(n) < 2 { 534 plural = "" 535 } 536 logger.Printf("Sending %s notification to: %d listener%s\n", 537 operation, len(n), plural) 538 } 539 for _, sendChannel := range n { 540 go func(channel chan<- string) { 541 channel <- name 542 }(sendChannel) 543 } 544 } 545 546 func (n makeDirectoryNotifiers) sendMakeDirectory(dir image.Directory, 547 logger log.Logger) { 548 if len(n) < 1 { 549 return 550 } else { 551 plural := "s" 552 if len(n) < 2 { 553 plural = "" 554 } 555 logger.Printf("Sending mkdir notification to: %d listener%s\n", 556 len(n), plural) 557 } 558 for _, sendChannel := range n { 559 go func(channel chan<- image.Directory) { 560 channel <- dir 561 }(sendChannel) 562 } 563 }