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  }