github.com/Cloud-Foundations/Dominator@v0.3.4/cmd/imagetool/lib.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/gob"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net"
    10  	"net/http"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"time"
    15  
    16  	hyperclient "github.com/Cloud-Foundations/Dominator/hypervisor/client"
    17  	imgclient "github.com/Cloud-Foundations/Dominator/imageserver/client"
    18  	"github.com/Cloud-Foundations/Dominator/lib/constants"
    19  	"github.com/Cloud-Foundations/Dominator/lib/filesystem"
    20  	"github.com/Cloud-Foundations/Dominator/lib/filter"
    21  	"github.com/Cloud-Foundations/Dominator/lib/image"
    22  	"github.com/Cloud-Foundations/Dominator/lib/image/packageutil"
    23  	"github.com/Cloud-Foundations/Dominator/lib/srpc"
    24  	"github.com/Cloud-Foundations/Dominator/lib/triggers"
    25  	fm_proto "github.com/Cloud-Foundations/Dominator/proto/fleetmanager"
    26  	img_proto "github.com/Cloud-Foundations/Dominator/proto/imageserver"
    27  	"github.com/Cloud-Foundations/Dominator/proto/sub"
    28  	subclient "github.com/Cloud-Foundations/Dominator/sub/client"
    29  )
    30  
    31  const (
    32  	imageTypeDirectory = iota
    33  	imageTypeFileSystem
    34  	imageTypeImage
    35  	imageTypeLatestImage
    36  	imageTypeImageFile
    37  	imageTypeSub
    38  	imageTypeVM
    39  )
    40  
    41  type typedImage struct {
    42  	buildLog   *image.Annotation
    43  	fileSystem *filesystem.FileSystem
    44  	filter     *filter.Filter
    45  	image      *image.Image
    46  	imageName  string
    47  	imageType  uint
    48  	specifier  string
    49  	triggers   *triggers.Triggers
    50  }
    51  
    52  type readCloser struct {
    53  	closer io.Closer
    54  	reader io.Reader
    55  }
    56  
    57  func dialMdbd() (*srpc.Client, error) {
    58  	clientName := fmt.Sprintf("%s:%d",
    59  		*mdbServerHostname, *mdbServerPortNum)
    60  	mdbdClient, err := srpc.DialHTTP("tcp", clientName, 0)
    61  	if err != nil {
    62  		return nil, fmt.Errorf("error dialing: %s: %s\n", clientName, err)
    63  	}
    64  	return mdbdClient, err
    65  }
    66  
    67  // getTypedFileReader returns a file reader. The reader must be closed before
    68  // the next call to getTypedFileReader.
    69  func getTypedFileReader(typedName, filename string) (io.ReadCloser, error) {
    70  	ti, err := makeTypedImage(typedName)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	return ti.openFile(filename)
    75  }
    76  
    77  func getTypedFileSystem(typedName string) (*filesystem.FileSystem, error) {
    78  	ti, err := getTypedImageType(typedName)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	fs, err := ti.getFileSystem()
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	return fs, nil
    87  }
    88  
    89  func getTypedFileSystemAndFilter(typedName string) (
    90  	*filesystem.FileSystem, *filter.Filter, error) {
    91  	ti, err := getTypedImageType(typedName)
    92  	if err != nil {
    93  		return nil, nil, err
    94  	}
    95  	fs, err := ti.getFileSystem()
    96  	if err != nil {
    97  		return nil, nil, err
    98  	}
    99  	return fs, ti.filter, nil
   100  }
   101  
   102  func getTypedImage(typedName string) (*image.Image, error) {
   103  	ti, err := getTypedImageType(typedName)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  	img, err := ti.getImage()
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	return img, nil
   112  }
   113  
   114  func getTypedImageAndName(typedName string) (*image.Image, string, error) {
   115  	ti, err := getTypedImageType(typedName)
   116  	if err != nil {
   117  		return nil, "", err
   118  	}
   119  	img, err := ti.getImage()
   120  	if err != nil {
   121  		return nil, "", err
   122  	}
   123  	name, err := ti.getImageName()
   124  	if err != nil {
   125  		return nil, "", err
   126  	}
   127  	return img, name, nil
   128  }
   129  
   130  func getTypedImageBuildLog(typedName string) (*image.Annotation, error) {
   131  	ti, err := makeTypedImage(typedName)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	if err := ti.loadMetadata(); err != nil {
   136  		return nil, err
   137  	}
   138  	buildLog, err := ti.getBuildLog()
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	return buildLog, nil
   143  }
   144  
   145  // getTypedImageBuildLogReader returns a build log reader. The reader must be
   146  // closed before the next call to getTypedImageBuildLogReader.
   147  func getTypedImageBuildLogReader(typedName string) (io.ReadCloser, error) {
   148  	buildLog, err := getTypedImageBuildLog(typedName)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	if hashPtr := buildLog.Object; hashPtr != nil {
   153  		_, objectClient := getClients()
   154  		_, r, err := objectClient.GetObject(*hashPtr)
   155  		if err != nil {
   156  			return nil, err
   157  		}
   158  		return r, nil
   159  	} else if buildLog.URL != "" {
   160  		resp, err := http.Get(buildLog.URL)
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  		if resp.StatusCode != http.StatusOK {
   165  			return nil, errors.New(resp.Status)
   166  		}
   167  		if resp.ContentLength > 0 {
   168  			return &readCloser{resp.Body,
   169  				&io.LimitedReader{resp.Body, resp.ContentLength}}, nil
   170  		}
   171  		return resp.Body, nil
   172  	} else {
   173  		return nil, errors.New("no build log data")
   174  	}
   175  }
   176  
   177  func getTypedImageFilter(typedName string) (*filter.Filter, error) {
   178  	ti, err := makeTypedImage(typedName)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  	if err := ti.loadMetadata(); err != nil {
   183  		return nil, err
   184  	}
   185  	filt, err := ti.getFilter()
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	return filt, nil
   190  }
   191  
   192  func getTypedImageMetadata(typedName string) (*image.Image, error) {
   193  	ti, err := makeTypedImage(typedName)
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  	if err := ti.loadMetadata(); err != nil {
   198  		return nil, err
   199  	}
   200  	img, err := ti.getImage()
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	return img, nil
   205  }
   206  
   207  func getTypedImageTriggers(typedName string) (*triggers.Triggers, error) {
   208  	ti, err := makeTypedImage(typedName)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	if err := ti.loadMetadata(); err != nil {
   213  		return nil, err
   214  	}
   215  	trig, err := ti.getTriggers()
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  	return trig, nil
   220  }
   221  
   222  func getTypedImageType(typedName string) (*typedImage, error) {
   223  	ti, err := makeTypedImage(typedName)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  	if err := ti.load(); err != nil {
   228  		return nil, err
   229  	}
   230  	return ti, nil
   231  }
   232  
   233  func getTypedPackageList(typedName string) ([]image.Package, error) {
   234  	ti, err := makeTypedImage(typedName)
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  	return ti.loadPackages()
   239  }
   240  
   241  func makeTypedImage(typedName string) (*typedImage, error) {
   242  	if len(typedName) < 3 || typedName[1] != ':' {
   243  		typedName = "i:" + typedName
   244  	}
   245  	var retval *typedImage
   246  	switch name := typedName[2:]; typedName[0] {
   247  	case 'd':
   248  		retval = &typedImage{imageType: imageTypeDirectory, specifier: name}
   249  	case 'f':
   250  		retval = &typedImage{imageType: imageTypeFileSystem, specifier: name}
   251  	case 'i':
   252  		retval = &typedImage{imageType: imageTypeImage, specifier: name}
   253  	case 'I':
   254  		retval = &typedImage{imageType: imageTypeLatestImage, specifier: name}
   255  	case 'l':
   256  		retval = &typedImage{imageType: imageTypeImageFile, specifier: name}
   257  	case 's':
   258  		retval = &typedImage{imageType: imageTypeSub, specifier: name}
   259  	case 'v':
   260  		retval = &typedImage{imageType: imageTypeVM, specifier: name}
   261  	default:
   262  		return nil, errors.New("unknown image type: " + typedName[:1])
   263  	}
   264  	return retval, nil
   265  }
   266  
   267  func (rc *readCloser) Close() error {
   268  	return rc.closer.Close()
   269  }
   270  
   271  func (rc *readCloser) Read(p []byte) (int, error) {
   272  	return rc.reader.Read(p)
   273  }
   274  
   275  func (ti *typedImage) getBuildLog() (*image.Annotation, error) {
   276  	if buildLog := ti.buildLog; buildLog == nil {
   277  		return nil, errors.New("BuildLog data not available")
   278  	} else {
   279  		return buildLog, nil
   280  	}
   281  }
   282  
   283  func (ti *typedImage) getFileSystem() (*filesystem.FileSystem, error) {
   284  	if fs := ti.fileSystem; fs == nil {
   285  		return nil, errors.New("FileSystem data not available")
   286  	} else {
   287  		return fs, nil
   288  	}
   289  }
   290  
   291  func (ti *typedImage) getFilter() (*filter.Filter, error) {
   292  	if filt := ti.filter; filt == nil {
   293  		return nil, errors.New("Filter not available")
   294  	} else {
   295  		return filt, nil
   296  	}
   297  }
   298  
   299  func (ti *typedImage) getImage() (*image.Image, error) {
   300  	if img := ti.image; img == nil {
   301  		return nil, errors.New("Image data not available")
   302  	} else {
   303  		return img, nil
   304  	}
   305  }
   306  
   307  func (ti *typedImage) getImageName() (string, error) {
   308  	if name := ti.imageName; name == "" {
   309  		return "", errors.New("Image name not available")
   310  	} else {
   311  		return name, nil
   312  	}
   313  }
   314  
   315  func (ti *typedImage) load() error {
   316  	switch ti.imageType {
   317  	case imageTypeDirectory:
   318  		fs, err := scanDirectory(ti.specifier)
   319  		if err != nil {
   320  			return err
   321  		}
   322  		ti.fileSystem = fs
   323  	case imageTypeFileSystem:
   324  		fs, err := readFileSystem(ti.specifier)
   325  		if err != nil {
   326  			return err
   327  		}
   328  		ti.fileSystem = fs
   329  	case imageTypeImage:
   330  		imageSClient, _ := getClients()
   331  		img, err := getImage(imageSClient, ti.specifier)
   332  		if err != nil {
   333  			return err
   334  		}
   335  		ti.buildLog = img.BuildLog
   336  		ti.fileSystem = img.FileSystem
   337  		ti.filter = img.Filter
   338  		ti.image = img
   339  		ti.imageName = ti.specifier
   340  		ti.triggers = img.Triggers
   341  	case imageTypeLatestImage:
   342  		imageSClient, _ := getClients()
   343  		img, name, err := getLatestImage(imageSClient, ti.specifier, false)
   344  		if err != nil {
   345  			return err
   346  		}
   347  		ti.buildLog = img.BuildLog
   348  		ti.fileSystem = img.FileSystem
   349  		ti.filter = img.Filter
   350  		ti.image = img
   351  		ti.imageName = name
   352  		ti.triggers = img.Triggers
   353  	case imageTypeImageFile:
   354  		img, err := readImage(ti.specifier)
   355  		if err != nil {
   356  			return err
   357  		}
   358  		ti.buildLog = img.BuildLog
   359  		ti.fileSystem = img.FileSystem
   360  		ti.filter = img.Filter
   361  		ti.image = img
   362  		ti.triggers = img.Triggers
   363  	case imageTypeSub:
   364  		fs, err := pollImage(ti.specifier)
   365  		if err != nil {
   366  			return err
   367  		}
   368  		ti.fileSystem = fs
   369  	case imageTypeVM:
   370  		fs, err := scanVm(ti.specifier)
   371  		if err != nil {
   372  			return err
   373  		}
   374  		ti.fileSystem = fs
   375  	default:
   376  		panic("unsupported typedImage in load()")
   377  	}
   378  	return nil
   379  }
   380  
   381  func (ti *typedImage) loadMetadata() error {
   382  	switch ti.imageType {
   383  	case imageTypeImage:
   384  		img, err := getImageMetadata(ti.specifier)
   385  		if err != nil {
   386  			return err
   387  		}
   388  		ti.buildLog = img.BuildLog
   389  		ti.filter = img.Filter
   390  		ti.image = img
   391  		ti.triggers = img.Triggers
   392  	case imageTypeLatestImage:
   393  		imageSClient, _ := getClients()
   394  		img, name, err := getLatestImage(imageSClient, ti.specifier, true)
   395  		if err != nil {
   396  			return err
   397  		}
   398  		ti.buildLog = img.BuildLog
   399  		ti.filter = img.Filter
   400  		ti.image = img
   401  		ti.imageName = name
   402  		ti.triggers = img.Triggers
   403  	case imageTypeImageFile:
   404  		img, err := readImage(ti.specifier)
   405  		if err != nil {
   406  			return err
   407  		}
   408  		ti.buildLog = img.BuildLog
   409  		ti.filter = img.Filter
   410  		ti.image = img
   411  		ti.triggers = img.Triggers
   412  	default:
   413  		return errors.New("package data not available")
   414  	}
   415  	return nil
   416  }
   417  
   418  func (ti *typedImage) loadPackages() ([]image.Package, error) {
   419  	switch ti.imageType {
   420  	case imageTypeDirectory:
   421  		return packageutil.GetPackageList(func(cmd string, w io.Writer) error {
   422  			command := exec.Command("/bin/generic-packager", cmd)
   423  			command.Stdout = w
   424  			return command.Run()
   425  		})
   426  	case imageTypeImage, imageTypeLatestImage, imageTypeImageFile:
   427  		if err := ti.loadMetadata(); err != nil {
   428  			return nil, err
   429  		}
   430  		return ti.image.Packages, nil
   431  	default:
   432  		return nil, errors.New("package data not available")
   433  	}
   434  }
   435  
   436  func (ti *typedImage) openFile(filename string) (io.ReadCloser, error) {
   437  	switch ti.imageType {
   438  	case imageTypeDirectory:
   439  		return os.Open(filepath.Join(ti.specifier, filename))
   440  	case imageTypeFileSystem, imageTypeImage, imageTypeLatestImage, imageTypeImageFile:
   441  		if err := ti.load(); err != nil {
   442  			return nil, err
   443  		}
   444  	case imageTypeSub:
   445  		data, err := readFileFromSub(ti.specifier, filename)
   446  		if err != nil {
   447  			return nil, err
   448  		}
   449  		return io.NopCloser(bytes.NewReader(data)), nil
   450  	default:
   451  		return nil, errors.New("unsupported typedImage in openFile()")
   452  	}
   453  	fs, err := ti.getFileSystem()
   454  	if err != nil {
   455  		return nil, err
   456  	}
   457  	filenameToInodeTable := fs.FilenameToInodeTable()
   458  	if inum, ok := filenameToInodeTable[filename]; !ok {
   459  		return nil, fmt.Errorf("file: \"%s\" not present in image", filename)
   460  	} else if inode, ok := fs.InodeTable[inum]; !ok {
   461  		return nil, fmt.Errorf("inode: %d not present in image", inum)
   462  	} else if inode, ok := inode.(*filesystem.RegularInode); !ok {
   463  		return nil, fmt.Errorf("file: \"%s\" is not a regular file", filename)
   464  	} else {
   465  		_, objectClient := getClients()
   466  		_, reader, err := objectClient.GetObject(inode.Hash)
   467  		if err != nil {
   468  			return nil, err
   469  		}
   470  		return reader, nil
   471  	}
   472  }
   473  
   474  func (ti *typedImage) getTriggers() (*triggers.Triggers, error) {
   475  	if trig := ti.triggers; trig == nil {
   476  		return nil, errors.New("Triggers not available")
   477  	} else {
   478  		return trig, nil
   479  	}
   480  }
   481  
   482  func findHypervisor(vmIpAddr net.IP) (string, error) {
   483  	if *hypervisorHostname != "" {
   484  		return fmt.Sprintf("%s:%d", *hypervisorHostname, *hypervisorPortNum),
   485  			nil
   486  	} else if *fleetManagerHostname != "" {
   487  		fm := fmt.Sprintf("%s:%d", *fleetManagerHostname, *fleetManagerPortNum)
   488  		client, err := srpc.DialHTTP("tcp", fm, time.Second*10)
   489  		if err != nil {
   490  			return "", err
   491  		}
   492  		defer client.Close()
   493  		return findHypervisorClient(client, vmIpAddr)
   494  	} else {
   495  		return fmt.Sprintf("localhost:%d", *hypervisorPortNum), nil
   496  	}
   497  }
   498  
   499  func findHypervisorClient(client *srpc.Client,
   500  	vmIpAddr net.IP) (string, error) {
   501  	request := fm_proto.GetHypervisorForVMRequest{vmIpAddr}
   502  	var reply fm_proto.GetHypervisorForVMResponse
   503  	err := client.RequestReply("FleetManager.GetHypervisorForVM", request,
   504  		&reply)
   505  	if err != nil {
   506  		return "", err
   507  	}
   508  	if err := errors.New(reply.Error); err != nil {
   509  		return "", err
   510  	}
   511  	return reply.HypervisorAddress, nil
   512  }
   513  
   514  func getImage(client *srpc.Client, name string) (*image.Image, error) {
   515  	img, err := imgclient.GetImageWithTimeout(client, name, *timeout)
   516  	if err != nil {
   517  		return nil, err
   518  	}
   519  	if img == nil {
   520  		return nil, errors.New(name + ": not found")
   521  	}
   522  	if err := img.FileSystem.RebuildInodePointers(); err != nil {
   523  		return nil, err
   524  	}
   525  	return img, nil
   526  }
   527  
   528  func getImageMetadata(imageName string) (*image.Image, error) {
   529  	imageSClient, _ := getClients()
   530  	logger.Debugf(0, "getting image: %s\n", imageName)
   531  	request := img_proto.GetImageRequest{
   532  		ImageName:        imageName,
   533  		IgnoreFilesystem: true,
   534  		Timeout:          *timeout,
   535  	}
   536  	var reply img_proto.GetImageResponse
   537  	err := imageSClient.RequestReply("ImageServer.GetImage", request, &reply)
   538  	if err != nil {
   539  		return nil, err
   540  	}
   541  	if reply.Image == nil {
   542  		return nil, fmt.Errorf("image: %s not found", imageName)
   543  	}
   544  	return reply.Image, nil
   545  }
   546  
   547  func getLatestImage(client *srpc.Client, name string,
   548  	ignoreFilesystem bool) (*image.Image, string, error) {
   549  	imageName, err := imgclient.FindLatestImageReq(client,
   550  		img_proto.FindLatestImageRequest{
   551  			BuildCommitId:        *buildCommitId,
   552  			DirectoryName:        name,
   553  			IgnoreExpiringImages: *ignoreExpiring,
   554  		})
   555  	if err != nil {
   556  		return nil, "", err
   557  	}
   558  	if ignoreFilesystem {
   559  		img, err := getImageMetadata(imageName)
   560  		if err != nil {
   561  			return nil, "", err
   562  		}
   563  		return img, imageName, nil
   564  	}
   565  	img, err := getImage(client, imageName)
   566  	if err != nil {
   567  		return nil, "", err
   568  	} else {
   569  		return img, imageName, nil
   570  	}
   571  }
   572  
   573  func getVmIpAndHypervisor(vmHostname string) (net.IP, *srpc.Client, error) {
   574  	vmIpAddr, err := lookupIP(vmHostname)
   575  	if err != nil {
   576  		return nil, nil, err
   577  	}
   578  	hypervisorAddress, err := findHypervisor(vmIpAddr)
   579  	if err != nil {
   580  		return nil, nil, err
   581  	}
   582  	client, err := srpc.DialHTTP("tcp", hypervisorAddress, time.Second*10)
   583  	if err != nil {
   584  		return nil, nil, err
   585  	}
   586  	return vmIpAddr, client, nil
   587  }
   588  
   589  func lookupIP(vmHostname string) (net.IP, error) {
   590  	if ips, err := net.LookupIP(vmHostname); err != nil {
   591  		return nil, err
   592  	} else if len(ips) != 1 {
   593  		return nil, fmt.Errorf("num IPs: %d != 1", len(ips))
   594  	} else {
   595  		return ips[0], nil
   596  	}
   597  }
   598  
   599  func pollImage(name string) (*filesystem.FileSystem, error) {
   600  	clientName := fmt.Sprintf("%s:%d", name, constants.SubPortNumber)
   601  	srpcClient, err := srpc.DialHTTP("tcp", clientName, 0)
   602  	if err != nil {
   603  		return nil, fmt.Errorf("error dialing %s", err)
   604  	}
   605  	defer srpcClient.Close()
   606  	var request sub.PollRequest
   607  	var reply sub.PollResponse
   608  	if err = subclient.CallPoll(srpcClient, request, &reply); err != nil {
   609  		return nil, err
   610  	}
   611  	if reply.FileSystem == nil {
   612  		return nil, errors.New("no poll data")
   613  	}
   614  	reply.FileSystem.RebuildInodePointers()
   615  	return reply.FileSystem, nil
   616  }
   617  
   618  func readFileFromSub(subHostname, filename string) ([]byte, error) {
   619  	clientName := fmt.Sprintf("%s:%d", subHostname, constants.SubPortNumber)
   620  	srpcClient, err := srpc.DialHTTP("tcp", clientName, 0)
   621  	if err != nil {
   622  		return nil, fmt.Errorf("error dialing %s", err)
   623  	}
   624  	defer srpcClient.Close()
   625  	buffer := &bytes.Buffer{}
   626  	err = subclient.GetFiles(srpcClient, []string{filename},
   627  		func(reader io.Reader, size uint64) error {
   628  			_, err := io.Copy(buffer, reader)
   629  			return err
   630  		})
   631  	if err != nil {
   632  		return nil, err
   633  	}
   634  	return buffer.Bytes(), nil
   635  }
   636  
   637  func readFileSystem(name string) (*filesystem.FileSystem, error) {
   638  	file, err := os.Open(name)
   639  	if err != nil {
   640  		return nil, err
   641  	}
   642  	defer file.Close()
   643  	var fileSystem filesystem.FileSystem
   644  	if err := gob.NewDecoder(file).Decode(&fileSystem); err != nil {
   645  		return nil, err
   646  	}
   647  	fileSystem.RebuildInodePointers()
   648  	return &fileSystem, nil
   649  }
   650  
   651  func readImage(name string) (*image.Image, error) {
   652  	file, err := os.Open(name)
   653  	if err != nil {
   654  		return nil, err
   655  	}
   656  	defer file.Close()
   657  	var img image.Image
   658  	if err := gob.NewDecoder(file).Decode(&img); err != nil {
   659  		return nil, err
   660  	}
   661  	img.FileSystem.RebuildInodePointers()
   662  	return &img, nil
   663  }
   664  
   665  func scanDirectory(name string) (*filesystem.FileSystem, error) {
   666  	fs, err := buildImageWithHasher(nil, nil, name, nil)
   667  	if err != nil {
   668  		return nil, err
   669  	}
   670  	return fs, nil
   671  }
   672  
   673  func scanVm(name string) (*filesystem.FileSystem, error) {
   674  	vmIpAddr, srpcClient, err := getVmIpAndHypervisor(name)
   675  	if err != nil {
   676  		return nil, err
   677  	}
   678  	defer srpcClient.Close()
   679  	fs, err := hyperclient.ScanVmRoot(srpcClient, vmIpAddr, nil)
   680  	if err != nil {
   681  		return nil, err
   682  	}
   683  	fs.RebuildInodePointers()
   684  	return fs, nil
   685  }