github.com/whamcloud/lemur@v0.0.0-20190827193804-4655df8a52af/cmd/lhsmd/agent/config.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 agent
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"flag"
    11  	"fmt"
    12  	"io/ioutil"
    13  	"os"
    14  	"path"
    15  	"runtime"
    16  	"strings"
    17  
    18  	"github.com/hashicorp/hcl"
    19  	"github.com/hashicorp/hcl/hcl/ast"
    20  	"github.com/pkg/errors"
    21  
    22  	"github.com/intel-hpdd/lemur/cmd/lhsmd/config"
    23  	"github.com/intel-hpdd/logging/alert"
    24  	"github.com/intel-hpdd/logging/debug"
    25  	"github.com/intel-hpdd/go-lustre/fs/spec"
    26  )
    27  
    28  var (
    29  	optConfigPath string
    30  )
    31  
    32  type (
    33  	transportConfig struct {
    34  		Type      string `hcl:"type"`
    35  		SocketDir string `hcl:"socket_dir"`
    36  	}
    37  
    38  	influxConfig struct {
    39  		URL      string `hcl:"url"`
    40  		DB       string `hcl:"db"`
    41  		User     string `hcl:"user"`
    42  		Password string `hcl:"password"`
    43  	}
    44  
    45  	snapshotConfig struct {
    46  		Enabled bool `hcl:"enabled"`
    47  	}
    48  
    49  	clientMountOptions []string
    50  
    51  	// Config represents HSM Agent configuration
    52  	Config struct {
    53  		MountRoot          string             `hcl:"mount_root" json:"mount_root"`
    54  		ClientDevice       *spec.ClientDevice `json:"client_device"`
    55  		ClientMountOptions clientMountOptions `hcl:"client_mount_options" json:"client_mount_options"`
    56  
    57  		Processes int `hcl:"handler_count" json:"handler_count"`
    58  
    59  		InfluxDB *influxConfig `hcl:"influxdb" json:"influxdb"`
    60  
    61  		EnabledPlugins []string `hcl:"enabled_plugins" json:"enabled_plugins"`
    62  		PluginDir      string   `hcl:"plugin_dir" json:"plugin_dir"`
    63  
    64  		Snapshots *snapshotConfig  `hcl:"snapshots" json:"snapshots"`
    65  		Transport *transportConfig `hcl:"transport" json:"transport"`
    66  	}
    67  )
    68  
    69  func (cmo clientMountOptions) HasOption(o string) bool {
    70  	for _, option := range cmo {
    71  		if option == o {
    72  			return true
    73  		}
    74  	}
    75  	return false
    76  }
    77  
    78  func (cmo clientMountOptions) String() string {
    79  	return strings.Join(cmo, ",")
    80  }
    81  
    82  func (c *transportConfig) Merge(other *transportConfig) *transportConfig {
    83  	result := new(transportConfig)
    84  
    85  	result.Type = c.Type
    86  	if other.Type != "" {
    87  		result.Type = other.Type
    88  	}
    89  
    90  	result.SocketDir = c.SocketDir
    91  	if other.SocketDir != "" {
    92  		result.SocketDir = other.SocketDir
    93  	}
    94  
    95  	return result
    96  }
    97  
    98  func (c *transportConfig) ConnectionString() string {
    99  	return fmt.Sprintf("%s/lhsmd-%d", c.SocketDir, os.Getpid())
   100  }
   101  
   102  func (c *influxConfig) Merge(other *influxConfig) *influxConfig {
   103  	result := new(influxConfig)
   104  
   105  	result.URL = c.URL
   106  	if other.URL != "" {
   107  		result.URL = other.URL
   108  	}
   109  
   110  	result.DB = c.DB
   111  	if other.DB != "" {
   112  		result.DB = other.DB
   113  	}
   114  
   115  	result.User = c.User
   116  	if other.User != "" {
   117  		result.User = other.User
   118  	}
   119  
   120  	result.Password = c.Password
   121  	if other.Password != "" {
   122  		result.Password = other.Password
   123  	}
   124  
   125  	return result
   126  }
   127  
   128  func (c *snapshotConfig) Merge(other *snapshotConfig) *snapshotConfig {
   129  	result := new(snapshotConfig)
   130  
   131  	result.Enabled = other.Enabled
   132  
   133  	return result
   134  }
   135  
   136  func init() {
   137  	flag.StringVar(&optConfigPath, "config", config.DefaultConfigPath, "Path to agent config")
   138  
   139  	// The CLI argument takes precedence, if both are set.
   140  	if optConfigPath == config.DefaultConfigPath {
   141  		if cfgDir := os.Getenv(config.ConfigDirEnvVar); cfgDir != "" {
   142  			optConfigPath = path.Join(cfgDir, config.AgentConfigFile)
   143  		}
   144  	}
   145  }
   146  
   147  func (c *Config) String() string {
   148  	data, err := json.Marshal(c)
   149  	if err != nil {
   150  		alert.Abort(errors.Wrap(err, "marshal failed"))
   151  	}
   152  
   153  	var out bytes.Buffer
   154  	json.Indent(&out, data, "", "\t")
   155  	return out.String()
   156  }
   157  
   158  // Plugins returns a slice of *PluginConfig instances for enabled plugins
   159  func (c *Config) Plugins() []*PluginConfig {
   160  	var plugins []*PluginConfig
   161  
   162  	// Ensure that this is set in our env so that plugins can use it to
   163  	// find their own configs
   164  	os.Setenv(config.ConfigDirEnvVar, path.Dir(optConfigPath))
   165  
   166  	connectAt := c.Transport.ConnectionString()
   167  	for _, name := range c.EnabledPlugins {
   168  		binPath := path.Join(c.PluginDir, name)
   169  		plugin := NewPlugin(name, binPath, connectAt, c.MountRoot)
   170  		plugins = append(plugins, plugin)
   171  	}
   172  
   173  	return plugins
   174  }
   175  
   176  // AgentMountpoint returns the calculated agent mountpoint under the
   177  // agent mount root.
   178  func (c *Config) AgentMountpoint() string {
   179  	return path.Join(c.MountRoot, "agent")
   180  }
   181  
   182  // Merge combines the supplied configuration's values with this one's
   183  func (c *Config) Merge(other *Config) *Config {
   184  	result := new(Config)
   185  
   186  	result.MountRoot = c.MountRoot
   187  	if other.MountRoot != "" {
   188  		result.MountRoot = other.MountRoot
   189  	}
   190  
   191  	result.ClientDevice = c.ClientDevice
   192  	if other.ClientDevice != nil {
   193  		result.ClientDevice = other.ClientDevice
   194  	}
   195  
   196  	result.ClientMountOptions = c.ClientMountOptions
   197  	for _, otherOption := range other.ClientMountOptions {
   198  		if result.ClientMountOptions.HasOption(otherOption) {
   199  			continue
   200  		}
   201  		result.ClientMountOptions = append(result.ClientMountOptions, otherOption)
   202  	}
   203  
   204  	result.Processes = c.Processes
   205  	if other.Processes > result.Processes {
   206  		result.Processes = other.Processes
   207  	}
   208  
   209  	result.InfluxDB = c.InfluxDB
   210  	if other.InfluxDB != nil {
   211  		result.InfluxDB = result.InfluxDB.Merge(other.InfluxDB)
   212  	}
   213  
   214  	result.EnabledPlugins = c.EnabledPlugins
   215  	if len(other.EnabledPlugins) > 0 {
   216  		result.EnabledPlugins = other.EnabledPlugins
   217  	}
   218  
   219  	result.PluginDir = c.PluginDir
   220  	if other.PluginDir != "" {
   221  		result.PluginDir = other.PluginDir
   222  	}
   223  
   224  	result.Snapshots = c.Snapshots
   225  	if other.Snapshots != nil {
   226  		result.Snapshots = result.Snapshots.Merge(other.Snapshots)
   227  	}
   228  
   229  	result.Transport = c.Transport
   230  	if other.Transport != nil {
   231  		result.Transport = result.Transport.Merge(other.Transport)
   232  	}
   233  
   234  	return result
   235  }
   236  
   237  // DefaultConfig initializes a new Config struct with default values
   238  func DefaultConfig() *Config {
   239  	cfg := NewConfig()
   240  	cfg.MountRoot = config.DefaultAgentMountRoot
   241  	cfg.ClientMountOptions = config.DefaultClientMountOptions
   242  	cfg.PluginDir = config.DefaultPluginDir
   243  	cfg.Processes = runtime.NumCPU()
   244  	cfg.Transport = &transportConfig{
   245  		Type:      config.DefaultTransport,
   246  		SocketDir: config.DefaultTransportSocketDir,
   247  	}
   248  	return cfg
   249  }
   250  
   251  // NewConfig initializes a new Config struct with zero values
   252  func NewConfig() *Config {
   253  	return &Config{
   254  		InfluxDB:           &influxConfig{},
   255  		Snapshots:          &snapshotConfig{},
   256  		Transport:          &transportConfig{},
   257  		EnabledPlugins:     []string{},
   258  		ClientMountOptions: clientMountOptions{},
   259  	}
   260  }
   261  
   262  // LoadConfig reads a config at the supplied path
   263  func LoadConfig(configPath string) (*Config, error) {
   264  	data, err := ioutil.ReadFile(configPath)
   265  	if err != nil {
   266  		return nil, errors.Wrap(err, "read failed")
   267  	}
   268  
   269  	obj, err := hcl.Parse(string(data))
   270  	if err != nil {
   271  		return nil, errors.Wrap(err, "parse config failed")
   272  	}
   273  
   274  	defaults := DefaultConfig()
   275  	cfg := NewConfig()
   276  	if err = hcl.DecodeObject(cfg, obj); err != nil {
   277  		return nil, errors.Wrap(err, "decode config failed")
   278  	}
   279  	cfg = defaults.Merge(cfg)
   280  
   281  	list, ok := obj.Node.(*ast.ObjectList)
   282  	if !ok {
   283  		return nil, errors.Errorf("Malformed config file")
   284  	}
   285  
   286  	f := list.Filter("client_device")
   287  	if len(f.Items) == 0 {
   288  		return nil, errors.Errorf("No client_device specified")
   289  	}
   290  	if len(f.Items) > 1 {
   291  		return nil, errors.Errorf("Line %d: More than 1 client_device specified", f.Items[1].Assign.Line)
   292  	}
   293  
   294  	var devStr string
   295  	if err = hcl.DecodeObject(&devStr, f.Elem().Items[0].Val); err != nil {
   296  		return nil, errors.Wrap(err, "decode device failed")
   297  	}
   298  	cfg.ClientDevice, err = spec.ClientDeviceFromString(devStr)
   299  	if err != nil {
   300  		return nil, errors.Wrapf(err, "Line %d: Invalid client_device %q", f.Items[0].Assign.Line, devStr)
   301  	}
   302  
   303  	return cfg, nil
   304  }
   305  
   306  // ConfigInitMust returns a valid *Config or fails trying
   307  func ConfigInitMust() *Config {
   308  	debug.Printf("loading config from %s", optConfigPath)
   309  	cfg, err := LoadConfig(optConfigPath)
   310  	if err != nil {
   311  		if !(optConfigPath == config.DefaultConfigPath && os.IsNotExist(err)) {
   312  			alert.Abort(errors.Wrap(err, "Failed to load config"))
   313  		}
   314  	}
   315  
   316  	if cfg.Transport == nil {
   317  		alert.Abort(errors.New("Invalid configuration: No transports configured"))
   318  	}
   319  
   320  	if _, err := os.Stat(cfg.PluginDir); os.IsNotExist(err) {
   321  		alert.Abort(errors.Errorf("Invalid configuration: plugin_dir %q does not exist", cfg.PluginDir))
   322  	}
   323  
   324  	if len(cfg.EnabledPlugins) == 0 {
   325  		alert.Abort(errors.New("Invalid configuration: No data mover plugins configured"))
   326  	}
   327  
   328  	for _, plugin := range cfg.EnabledPlugins {
   329  		pluginPath := path.Join(cfg.PluginDir, plugin)
   330  		if _, err := os.Stat(pluginPath); os.IsNotExist(err) {
   331  			alert.Abort(errors.Errorf("Invalid configuration: Plugin %q not found in %s", plugin, cfg.PluginDir))
   332  		}
   333  	}
   334  
   335  	return cfg
   336  }