github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/environs/bootstrap/config.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package bootstrap
     5  
     6  import (
     7  	"crypto/rand"
     8  	"crypto/tls"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  	"time"
    15  
    16  	"github.com/juju/errors"
    17  	"github.com/juju/schema"
    18  	"github.com/juju/utils"
    19  
    20  	"github.com/juju/juju/cert"
    21  	"github.com/juju/juju/juju/osenv"
    22  )
    23  
    24  const (
    25  	// AdminSecretKey is the attribute key for the administrator password.
    26  	AdminSecretKey = "admin-secret"
    27  
    28  	// CACertKey is the attribute key for the controller's CA certificate.
    29  	CACertKey = "ca-cert"
    30  
    31  	// CAPrivateKeyKey is the key for the controller's CA certificate private key.
    32  	CAPrivateKeyKey = "ca-private-key"
    33  
    34  	// BootstrapTimeoutKey is the attribute key for the amount of time to wait
    35  	// for bootstrap to complete.
    36  	BootstrapTimeoutKey = "bootstrap-timeout"
    37  
    38  	// BootstrapRetryDelayKey is the attribute key for the amount of time
    39  	// in between attempts to connect to a bootstrap machine address.
    40  	BootstrapRetryDelayKey = "bootstrap-retry-delay"
    41  
    42  	// BootstrapAddressesDelayKey is the attribute key for the amount of
    43  	// time in between refreshing the bootstrap machine addresses.
    44  	BootstrapAddressesDelayKey = "bootstrap-addresses-delay"
    45  )
    46  
    47  const (
    48  	// Attribute Defaults
    49  
    50  	// DefaultBootstrapSSHTimeout is the amount of time to wait
    51  	// contacting a controller, in seconds.
    52  	DefaultBootstrapSSHTimeout = 1200
    53  
    54  	// DefaultBootstrapSSHRetryDelay is the amount of time between
    55  	// attempts to connect to an address, in seconds.
    56  	DefaultBootstrapSSHRetryDelay = 5
    57  
    58  	// DefaultBootstrapSSHAddressesDelay is the amount of time betwee
    59  	// refreshing the addresses, in seconds. Not too frequent, as we
    60  	// refresh addresses from the provider each time.
    61  	DefaultBootstrapSSHAddressesDelay = 10
    62  )
    63  
    64  // BootstrapConfigAttributes are attributes which may be defined by the
    65  // user at bootstrap time, but should not be present in general controller
    66  // config.
    67  var BootstrapConfigAttributes = []string{
    68  	AdminSecretKey,
    69  	CACertKey,
    70  	CAPrivateKeyKey,
    71  	BootstrapTimeoutKey,
    72  	BootstrapRetryDelayKey,
    73  	BootstrapAddressesDelayKey,
    74  }
    75  
    76  // IsBootstrapAttribute reports whether or not the specified
    77  // attribute name is only relevant during bootstrap.
    78  func IsBootstrapAttribute(attr string) bool {
    79  	for _, a := range BootstrapConfigAttributes {
    80  		if attr == a {
    81  			return true
    82  		}
    83  	}
    84  	return false
    85  }
    86  
    87  // Config contains bootstrap-specific configuration.
    88  type Config struct {
    89  	AdminSecret             string
    90  	CACert                  string
    91  	CAPrivateKey            string
    92  	BootstrapTimeout        time.Duration
    93  	BootstrapRetryDelay     time.Duration
    94  	BootstrapAddressesDelay time.Duration
    95  }
    96  
    97  // Validate validates the controller configuration.
    98  func (c Config) Validate() error {
    99  	if c.AdminSecret == "" {
   100  		return errors.NotValidf("empty " + AdminSecretKey)
   101  	}
   102  	if _, err := tls.X509KeyPair([]byte(c.CACert), []byte(c.CAPrivateKey)); err != nil {
   103  		return errors.Annotatef(err, "validating %s and %s", CACertKey, CAPrivateKeyKey)
   104  	}
   105  	if c.BootstrapTimeout <= 0 {
   106  		return errors.NotValidf("%s of %s", BootstrapTimeoutKey, c.BootstrapTimeout)
   107  	}
   108  	if c.BootstrapRetryDelay <= 0 {
   109  		return errors.NotValidf("%s of %s", BootstrapRetryDelayKey, c.BootstrapRetryDelay)
   110  	}
   111  	if c.BootstrapAddressesDelay <= 0 {
   112  		return errors.NotValidf("%s of %s", BootstrapAddressesDelayKey, c.BootstrapAddressesDelay)
   113  	}
   114  	return nil
   115  }
   116  
   117  // NewConfig creates a new Config from the supplied attributes.
   118  // Default values will be used where defaults are available.
   119  //
   120  // If ca-cert or ca-private-key are not set, then we will check
   121  // if ca-cert-path or ca-private-key-path are set, and read the
   122  // contents. If none of those are set, we will look for files
   123  // in well-defined locations: $JUJU_DATA/ca-cert.pem, and
   124  // $JUJU_DATA/ca-private-key.pem. If none of these are set, an
   125  // error is returned.
   126  func NewConfig(attrs map[string]interface{}) (Config, error) {
   127  	coerced, err := configChecker.Coerce(attrs, nil)
   128  	if err != nil {
   129  		return Config{}, errors.Trace(err)
   130  	}
   131  	attrs = coerced.(map[string]interface{})
   132  	config := Config{
   133  		BootstrapTimeout:        time.Duration(attrs[BootstrapTimeoutKey].(int)) * time.Second,
   134  		BootstrapRetryDelay:     time.Duration(attrs[BootstrapRetryDelayKey].(int)) * time.Second,
   135  		BootstrapAddressesDelay: time.Duration(attrs[BootstrapAddressesDelayKey].(int)) * time.Second,
   136  	}
   137  
   138  	if adminSecret, ok := attrs[AdminSecretKey].(string); ok {
   139  		config.AdminSecret = adminSecret
   140  	} else {
   141  		// Generate a random admin secret.
   142  		buf := make([]byte, 16)
   143  		if _, err := io.ReadFull(rand.Reader, buf); err != nil {
   144  			return Config{}, errors.Annotate(err, "generating random "+AdminSecretKey)
   145  		}
   146  		config.AdminSecret = fmt.Sprintf("%x", buf)
   147  	}
   148  
   149  	if caCert, ok := attrs[CACertKey].(string); ok {
   150  		config.CACert = caCert
   151  	} else {
   152  		var userSpecified bool
   153  		var err error
   154  		config.CACert, userSpecified, err = readFileAttr(attrs, CACertKey, CACertKey+".pem")
   155  		if err != nil && (userSpecified || !os.IsNotExist(errors.Cause(err))) {
   156  			return Config{}, errors.Annotatef(err, "reading %q from file", CACertKey)
   157  		}
   158  	}
   159  
   160  	if caPrivateKey, ok := attrs[CAPrivateKeyKey].(string); ok {
   161  		config.CAPrivateKey = caPrivateKey
   162  	} else {
   163  		var userSpecified bool
   164  		var err error
   165  		config.CAPrivateKey, userSpecified, err = readFileAttr(attrs, CAPrivateKeyKey, CAPrivateKeyKey+".pem")
   166  		if err != nil && (userSpecified || !os.IsNotExist(errors.Cause(err))) {
   167  			return Config{}, errors.Annotatef(err, "reading %q from file", CAPrivateKeyKey)
   168  		}
   169  	}
   170  
   171  	if config.CACert == "" && config.CAPrivateKey == "" {
   172  		// Generate a new CA certificate and private key.
   173  		// TODO(perrito666) 2016-05-02 lp:1558657
   174  		expiry := time.Now().UTC().AddDate(10, 0, 0)
   175  		uuid, err := utils.NewUUID()
   176  		if err != nil {
   177  			return Config{}, errors.Annotate(err, "generating UUID for CA certificate")
   178  		}
   179  		caCert, caKey, err := cert.NewCA("juju-ca", uuid.String(), expiry)
   180  		if err != nil {
   181  			return Config{}, errors.Trace(err)
   182  		}
   183  		config.CACert = caCert
   184  		config.CAPrivateKey = caKey
   185  	}
   186  
   187  	return config, config.Validate()
   188  }
   189  
   190  // readFileAttr reads the contents of an attribute from a file, if the
   191  // corresponding "-path" attribute is set, or otherwise from a default
   192  // path.
   193  func readFileAttr(attrs map[string]interface{}, key, defaultPath string) (content string, userSpecified bool, _ error) {
   194  	path, ok := attrs[key+"-path"].(string)
   195  	if ok {
   196  		userSpecified = true
   197  	} else {
   198  		path = defaultPath
   199  	}
   200  	absPath, err := utils.NormalizePath(path)
   201  	if err != nil {
   202  		return "", userSpecified, errors.Trace(err)
   203  	}
   204  	if !filepath.IsAbs(absPath) {
   205  		absPath = osenv.JujuXDGDataHomePath(absPath)
   206  	}
   207  	data, err := ioutil.ReadFile(absPath)
   208  	if err != nil {
   209  		return "", userSpecified, errors.Annotatef(err, "%q not set, and could not read from %q", key, path)
   210  	}
   211  	if len(data) == 0 {
   212  		return "", userSpecified, errors.Errorf("file %q is empty", path)
   213  	}
   214  	return string(data), userSpecified, nil
   215  }
   216  
   217  var configChecker = schema.FieldMap(schema.Fields{
   218  	AdminSecretKey:             schema.String(),
   219  	CACertKey:                  schema.String(),
   220  	CACertKey + "-path":        schema.String(),
   221  	CAPrivateKeyKey:            schema.String(),
   222  	CAPrivateKeyKey + "-path":  schema.String(),
   223  	BootstrapTimeoutKey:        schema.ForceInt(),
   224  	BootstrapRetryDelayKey:     schema.ForceInt(),
   225  	BootstrapAddressesDelayKey: schema.ForceInt(),
   226  }, schema.Defaults{
   227  	AdminSecretKey:             schema.Omit,
   228  	CACertKey:                  schema.Omit,
   229  	CACertKey + "-path":        schema.Omit,
   230  	CAPrivateKeyKey:            schema.Omit,
   231  	CAPrivateKeyKey + "-path":  schema.Omit,
   232  	BootstrapTimeoutKey:        DefaultBootstrapSSHTimeout,
   233  	BootstrapRetryDelayKey:     DefaultBootstrapSSHRetryDelay,
   234  	BootstrapAddressesDelayKey: DefaultBootstrapSSHAddressesDelay,
   235  })