github.com/whamcloud/lemur@v0.0.0-20190827193804-4655df8a52af/cmd/lhsm-plugin-s3/main.go (about)

     1  // Copyright (c) 2018 DDN. All rights reserved.
     2  // Use of this source code is governed by a MIT-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"os/signal"
    11  	"path"
    12  	"strings"
    13  	"syscall"
    14  	"time"
    15  
    16  	"github.com/aws/aws-sdk-go/aws"
    17  	"github.com/aws/aws-sdk-go/aws/credentials"
    18  	"github.com/aws/aws-sdk-go/aws/defaults"
    19  	"github.com/aws/aws-sdk-go/aws/session"
    20  	"github.com/aws/aws-sdk-go/service/s3"
    21  	"github.com/aws/aws-sdk-go/service/s3/s3manager"
    22  	"github.com/dustin/go-humanize"
    23  	"github.com/pkg/errors"
    24  	"github.com/rcrowley/go-metrics"
    25  
    26  	"github.com/intel-hpdd/lemur/dmplugin"
    27  	"github.com/intel-hpdd/lemur/pkg/fsroot"
    28  	"github.com/intel-hpdd/logging/alert"
    29  	"github.com/intel-hpdd/logging/audit"
    30  	"github.com/intel-hpdd/logging/debug"
    31  )
    32  
    33  type (
    34  	archiveConfig struct {
    35  		Name               string `hcl:",key"`
    36  		ID                 int
    37  		AWSAccessKeyID     string `hcl:"aws_access_key_id"`
    38  		AWSSecretAccessKey string `hcl:"aws_secret_access_key"`
    39  		Endpoint           string
    40  		Region             string
    41  		Bucket             string
    42  		Prefix             string
    43  		UploadPartSize     int64 `hcl:"upload_part_size"`
    44  
    45  		s3Creds *credentials.Credentials
    46  	}
    47  
    48  	archiveSet []*archiveConfig
    49  
    50  	s3Config struct {
    51  		NumThreads         int        `hcl:"num_threads"`
    52  		AWSAccessKeyID     string     `hcl:"aws_access_key_id"`
    53  		AWSSecretAccessKey string     `hcl:"aws_secret_access_key"`
    54  		Endpoint           string     `hcl:"endpoint"`
    55  		Region             string     `hcl:"region"`
    56  		UploadPartSize     int64      `hcl:"upload_part_size"`
    57  		Archives           archiveSet `hcl:"archive"`
    58  	}
    59  )
    60  
    61  // Should this be configurable?
    62  const updateInterval = 10 * time.Second
    63  
    64  var rate metrics.Meter
    65  
    66  func (c *s3Config) String() string {
    67  	return dmplugin.DisplayConfig(c)
    68  }
    69  
    70  func (a *archiveConfig) String() string {
    71  	return fmt.Sprintf("%d:%s:%s:%s/%s", a.ID, a.Endpoint, a.Region, a.Bucket, a.Prefix)
    72  }
    73  
    74  func (a *archiveConfig) checkValid() error {
    75  	var errors []string
    76  
    77  	if a.Bucket == "" {
    78  		errors = append(errors, fmt.Sprintf("Archive %s: bucket not set", a.Name))
    79  	}
    80  
    81  	if a.ID < 1 {
    82  		errors = append(errors, fmt.Sprintf("Archive %s: archive id not set", a.Name))
    83  
    84  	}
    85  
    86  	if a.UploadPartSize < s3manager.MinUploadPartSize {
    87  		errors = append(errors, fmt.Sprintf("Archive %s: upload_part_size %d is less than minimum (%d)", a.Name, a.UploadPartSize, s3manager.MinUploadPartSize))
    88  	}
    89  
    90  	if len(errors) > 0 {
    91  		return fmt.Errorf("Errors: %s", strings.Join(errors, ", "))
    92  	}
    93  
    94  	return nil
    95  }
    96  
    97  func (a *archiveConfig) checkS3Access() error {
    98  	if _, err := a.s3Creds.Get(); err != nil {
    99  		return errors.Wrap(err, "No S3 credentials found; cannot initialize data mover")
   100  	}
   101  
   102  	if _, err := s3Svc(a).ListObjects(&s3.ListObjectsInput{
   103  		Bucket: aws.String(a.Bucket),
   104  	}); err != nil {
   105  		return errors.Wrap(err, "Unable to list S3 bucket objects")
   106  	}
   107  
   108  	return nil
   109  }
   110  
   111  // this does an in-place merge, replacing any unset archive-level value
   112  // with the global value for that setting
   113  func (a *archiveConfig) mergeGlobals(g *s3Config) {
   114  	if a.AWSAccessKeyID == "" {
   115  		a.AWSAccessKeyID = g.AWSAccessKeyID
   116  	}
   117  
   118  	if a.AWSSecretAccessKey == "" {
   119  		a.AWSSecretAccessKey = g.AWSSecretAccessKey
   120  	}
   121  
   122  	if a.Endpoint == "" {
   123  		a.Endpoint = g.Endpoint
   124  	}
   125  
   126  	if a.Region == "" {
   127  		a.Region = g.Region
   128  	}
   129  
   130  	if a.UploadPartSize == 0 {
   131  		a.UploadPartSize = g.UploadPartSize
   132  	} else {
   133  		// Allow this to be configured in MiB
   134  		a.UploadPartSize *= 1024 * 1024
   135  	}
   136  
   137  	// Set the default credentials provider
   138  	a.s3Creds = defaults.CredChain(
   139  		aws.NewConfig().WithRegion(a.Region), defaults.Handlers(),
   140  	)
   141  	// If these were set on a per-archive basis, override the defaults.
   142  	if a.AWSAccessKeyID != "" && a.AWSSecretAccessKey != "" {
   143  		a.s3Creds = credentials.NewStaticCredentials(
   144  			a.AWSAccessKeyID, a.AWSSecretAccessKey, "",
   145  		)
   146  	}
   147  }
   148  
   149  func (c *s3Config) Merge(other *s3Config) *s3Config {
   150  	result := new(s3Config)
   151  
   152  	result.UploadPartSize = c.UploadPartSize
   153  	if other.UploadPartSize > 0 {
   154  		result.UploadPartSize = other.UploadPartSize
   155  	}
   156  
   157  	result.NumThreads = c.NumThreads
   158  	if other.NumThreads > 0 {
   159  		result.NumThreads = other.NumThreads
   160  	}
   161  
   162  	result.Region = c.Region
   163  	if other.Region != "" {
   164  		result.Region = other.Region
   165  	}
   166  
   167  	result.Endpoint = c.Endpoint
   168  	if other.Endpoint != "" {
   169  		result.Endpoint = other.Endpoint
   170  	}
   171  
   172  	result.AWSAccessKeyID = c.AWSAccessKeyID
   173  	if other.AWSAccessKeyID != "" {
   174  		result.AWSAccessKeyID = other.AWSAccessKeyID
   175  	}
   176  
   177  	result.AWSSecretAccessKey = c.AWSSecretAccessKey
   178  	if other.AWSSecretAccessKey != "" {
   179  		result.AWSSecretAccessKey = other.AWSSecretAccessKey
   180  	}
   181  
   182  	result.Archives = c.Archives
   183  	if len(other.Archives) > 0 {
   184  		result.Archives = other.Archives
   185  	}
   186  
   187  	return result
   188  }
   189  
   190  func init() {
   191  	rate = metrics.NewMeter()
   192  
   193  	// if debug.Enabled() {
   194  	go func() {
   195  		var lastCount int64
   196  		for {
   197  			if lastCount != rate.Count() {
   198  				audit.Logf("total %s (1 min/5 min/15 min/inst): %s/%s/%s/%s msg/sec\n",
   199  					humanize.Comma(rate.Count()),
   200  					humanize.Comma(int64(rate.Rate1())),
   201  					humanize.Comma(int64(rate.Rate5())),
   202  					humanize.Comma(int64(rate.Rate15())),
   203  					humanize.Comma(int64(rate.RateMean())),
   204  				)
   205  				lastCount = rate.Count()
   206  			}
   207  			time.Sleep(10 * time.Second)
   208  		}
   209  	}()
   210  	// }
   211  }
   212  
   213  func s3Svc(ac *archiveConfig) *s3.S3 {
   214  	cfg := aws.NewConfig().WithRegion(ac.Region).WithCredentials(ac.s3Creds)
   215  	if debug.Enabled() {
   216  		cfg.WithLogger(debug.Writer())
   217  		cfg.WithLogLevel(aws.LogDebug)
   218  	}
   219  	if ac.Endpoint != "" {
   220  		cfg.WithEndpoint(ac.Endpoint)
   221  		cfg.WithS3ForcePathStyle(true)
   222  	}
   223  	return s3.New(session.New(cfg))
   224  }
   225  
   226  func getMergedConfig(plugin *dmplugin.Plugin) (*s3Config, error) {
   227  	baseCfg := &s3Config{
   228  		Region:         "us-east-1",
   229  		UploadPartSize: s3manager.DefaultUploadPartSize,
   230  	}
   231  
   232  	var cfg s3Config
   233  	err := dmplugin.LoadConfig(plugin.ConfigFile(), &cfg)
   234  
   235  	if err != nil {
   236  		return nil, fmt.Errorf("Failed to load config: %s", err)
   237  	}
   238  
   239  	// Allow this to be configured in MiB
   240  	if cfg.UploadPartSize != 0 {
   241  		cfg.UploadPartSize *= 1024 * 1024
   242  	}
   243  
   244  	return baseCfg.Merge(&cfg), nil
   245  }
   246  
   247  func main() {
   248  	plugin, err := dmplugin.New(path.Base(os.Args[0]), func(path string) (fsroot.Client, error) {
   249  		return fsroot.New(path)
   250  	})
   251  	if err != nil {
   252  		alert.Abort(errors.Wrap(err, "failed to initialize plugin"))
   253  	}
   254  	defer plugin.Close()
   255  
   256  	cfg, err := getMergedConfig(plugin)
   257  	if err != nil {
   258  		alert.Abort(errors.Wrap(err, "Unable to determine plugin configuration"))
   259  	}
   260  
   261  	if len(cfg.Archives) == 0 {
   262  		alert.Abort(errors.New("Invalid configuration: No archives defined"))
   263  	}
   264  
   265  	for _, ac := range cfg.Archives {
   266  		ac.mergeGlobals(cfg)
   267  		if err = ac.checkValid(); err != nil {
   268  			alert.Abort(errors.Wrap(err, "Invalid configuration"))
   269  		}
   270  		if err = ac.checkS3Access(); err != nil {
   271  			alert.Abort(errors.Wrap(err, "S3 access check failed"))
   272  		}
   273  	}
   274  
   275  	debug.Printf("S3Mover configuration:\n%v", cfg)
   276  
   277  	// All base filesystem operations will be relative to current directory
   278  	err = os.Chdir(plugin.Base())
   279  	if err != nil {
   280  		alert.Abort(errors.Wrap(err, "chdir failed"))
   281  	}
   282  
   283  	interruptHandler(func() {
   284  		plugin.Stop()
   285  	})
   286  
   287  	for _, ac := range cfg.Archives {
   288  		plugin.AddMover(&dmplugin.Config{
   289  			Mover:      S3Mover(ac, s3Svc(ac), uint32(ac.ID)),
   290  			NumThreads: cfg.NumThreads,
   291  			ArchiveID:  uint32(ac.ID),
   292  		})
   293  	}
   294  
   295  	plugin.Run()
   296  }
   297  
   298  func interruptHandler(once func()) {
   299  	c := make(chan os.Signal, 1)
   300  	signal.Notify(c, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM)
   301  
   302  	go func() {
   303  		stopping := false
   304  		for sig := range c {
   305  			debug.Printf("signal received: %s", sig)
   306  			if !stopping {
   307  				stopping = true
   308  				once()
   309  			}
   310  		}
   311  	}()
   312  }