github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/cloudconfig/instancecfg/instancecfg.go (about)

     1  // Copyright 2012, 2013, 2015 Canonical Ltd.
     2  // Copyright 2015 Cloudbase Solutions SRL
     3  // Licensed under the AGPLv3, see LICENCE file for details.
     4  
     5  package instancecfg
     6  
     7  import (
     8  	"fmt"
     9  	"net"
    10  	"path"
    11  	"reflect"
    12  	"strconv"
    13  
    14  	"github.com/juju/errors"
    15  	"github.com/juju/loggo"
    16  	"github.com/juju/names"
    17  	"github.com/juju/utils/proxy"
    18  	"github.com/juju/utils/shell"
    19  	"github.com/juju/version"
    20  
    21  	"github.com/juju/juju/agent"
    22  	agenttools "github.com/juju/juju/agent/tools"
    23  	"github.com/juju/juju/api"
    24  	"github.com/juju/juju/apiserver/params"
    25  	"github.com/juju/juju/constraints"
    26  	"github.com/juju/juju/environs/config"
    27  	"github.com/juju/juju/environs/imagemetadata"
    28  	"github.com/juju/juju/environs/tags"
    29  	"github.com/juju/juju/instance"
    30  	"github.com/juju/juju/juju/paths"
    31  	"github.com/juju/juju/mongo"
    32  	"github.com/juju/juju/service"
    33  	"github.com/juju/juju/service/common"
    34  	"github.com/juju/juju/state/multiwatcher"
    35  	coretools "github.com/juju/juju/tools"
    36  )
    37  
    38  var logger = loggo.GetLogger("juju.cloudconfig.instancecfg")
    39  
    40  // InstanceConfig represents initialization information for a new juju instance.
    41  type InstanceConfig struct {
    42  	// Tags is a set of tags to set on the instance, if supported. This
    43  	// should be populated using the InstanceTags method in this package.
    44  	Tags map[string]string
    45  
    46  	// Bootstrap specifies whether the new instance is the bootstrap
    47  	// instance. When this is true, StateServingInfo should be set
    48  	// and filled out.
    49  	Bootstrap bool
    50  
    51  	// StateServingInfo holds the information for serving the state.
    52  	// This must only be set if the Bootstrap field is true
    53  	// (controllers started subsequently will acquire their serving info
    54  	// from another server)
    55  	StateServingInfo *params.StateServingInfo
    56  
    57  	// MongoInfo holds the means for the new instance to communicate with the
    58  	// juju state database. Unless the new instance is running a controller
    59  	// (Controller is set), there must be at least one controller address supplied.
    60  	// The entity name must match that of the instance being started,
    61  	// or be empty when starting a controller.
    62  	MongoInfo *mongo.MongoInfo
    63  
    64  	// APIInfo holds the means for the new instance to communicate with the
    65  	// juju state API. Unless the new instance is running a controller (Controller is
    66  	// set), there must be at least one controller address supplied.
    67  	// The entity name must match that of the instance being started,
    68  	// or be empty when starting a controller.
    69  	APIInfo *api.Info
    70  
    71  	// InstanceId is the instance ID of the instance being initialised.
    72  	// This is required when bootstrapping, and ignored otherwise.
    73  	InstanceId instance.Id
    74  
    75  	// HardwareCharacteristics contains the harrdware characteristics of
    76  	// the instance being initialised. This optional, and is only used by
    77  	// the bootstrap agent during state initialisation.
    78  	HardwareCharacteristics *instance.HardwareCharacteristics
    79  
    80  	// MachineNonce is set at provisioning/bootstrap time and used to
    81  	// ensure the agent is running on the correct instance.
    82  	MachineNonce string
    83  
    84  	// tools is the list of juju tools used to install the Juju agent
    85  	// on the new instance. Each of the entries in the list must have
    86  	// identical versions and hashes, but may have different URLs.
    87  	tools coretools.List
    88  
    89  	// GUI is the Juju GUI archive to be installed in the new instance.
    90  	GUI *coretools.GUIArchive
    91  
    92  	// DataDir holds the directory that juju state will be put in the new
    93  	// instance.
    94  	DataDir string
    95  
    96  	// LogDir holds the directory that juju logs will be written to.
    97  	LogDir string
    98  
    99  	// MetricsSpoolDir represents the spool directory path, where all
   100  	// metrics are stored.
   101  	MetricsSpoolDir string
   102  
   103  	// Jobs holds what machine jobs to run.
   104  	Jobs []multiwatcher.MachineJob
   105  
   106  	// CloudInitOutputLog specifies the path to the output log for cloud-init.
   107  	// The directory containing the log file must already exist.
   108  	CloudInitOutputLog string
   109  
   110  	// MachineId identifies the new machine.
   111  	MachineId string
   112  
   113  	// MachineContainerType specifies the type of container that the instance
   114  	// is.  If the instance is not a container, then the type is "".
   115  	MachineContainerType instance.ContainerType
   116  
   117  	// MachineContainerHostname specifies the hostname to be used with the
   118  	// cloud config for the instance. If this is not set, hostname uses the default.
   119  	MachineContainerHostname string
   120  
   121  	// AuthorizedKeys specifies the keys that are allowed to
   122  	// connect to the instance (see cloudinit.SSHAddAuthorizedKeys)
   123  	// If no keys are supplied, there can be no ssh access to the node.
   124  	// On a bootstrap instance, that is fatal. On other
   125  	// instances it will mean that the ssh, scp and debug-hooks
   126  	// commands cannot work.
   127  	AuthorizedKeys string
   128  
   129  	// AgentEnvironment defines additional configuration variables to set in
   130  	// the instance agent config.
   131  	AgentEnvironment map[string]string
   132  
   133  	// WARNING: this is only set if the instance being configured is
   134  	// a controller node.
   135  	//
   136  	// Config holds the initial environment configuration.
   137  	Config *config.Config
   138  
   139  	// HostedModelConfig is a set of config attributes to be overlaid
   140  	// on the controller model config (Config, above) to construct the
   141  	// initial hosted model config.
   142  	HostedModelConfig map[string]interface{}
   143  
   144  	// Constraints holds the machine constraints.
   145  	Constraints constraints.Value
   146  
   147  	// ModelConstraints holds the initial model constraints.
   148  	ModelConstraints constraints.Value
   149  
   150  	// DisableSSLHostnameVerification can be set to true to tell cloud-init
   151  	// that it shouldn't verify SSL certificates
   152  	DisableSSLHostnameVerification bool
   153  
   154  	// Series represents the instance series.
   155  	Series string
   156  
   157  	// MachineAgentServiceName is the init service name for the Juju machine agent.
   158  	MachineAgentServiceName string
   159  
   160  	// ProxySettings define normal http, https and ftp proxies.
   161  	ProxySettings proxy.Settings
   162  
   163  	// AptProxySettings define the http, https and ftp proxy settings to use
   164  	// for apt, which may or may not be the same as the normal ProxySettings.
   165  	AptProxySettings proxy.Settings
   166  
   167  	// AptMirror defines an APT mirror location, which, if specified, will
   168  	// override the default APT sources.
   169  	AptMirror string
   170  
   171  	// PreferIPv6 mirrors the value of prefer-ipv6 environment setting
   172  	// and when set IPv6 addresses for connecting to the API/state
   173  	// servers will be preferred over IPv4 ones.
   174  	PreferIPv6 bool
   175  
   176  	// The type of Simple Stream to download and deploy on this instance.
   177  	ImageStream string
   178  
   179  	// The public key used to sign Juju simplestreams image metadata.
   180  	PublicImageSigningKey string
   181  
   182  	// CustomImageMetadata is optional custom simplestreams image metadata
   183  	// to store in environment storage at bootstrap time. This is ignored
   184  	// in non-bootstrap instances.
   185  	CustomImageMetadata []*imagemetadata.ImageMetadata
   186  
   187  	// EnableOSRefreshUpdate specifies whether Juju will refresh its
   188  	// respective OS's updates list.
   189  	EnableOSRefreshUpdate bool
   190  
   191  	// EnableOSUpgrade defines Juju's behavior when provisioning
   192  	// instances. If enabled, the OS will perform any upgrades
   193  	// available as part of its provisioning.
   194  	EnableOSUpgrade bool
   195  }
   196  
   197  func (cfg *InstanceConfig) agentInfo() service.AgentInfo {
   198  	return service.NewMachineAgentInfo(
   199  		cfg.MachineId,
   200  		cfg.DataDir,
   201  		cfg.LogDir,
   202  	)
   203  }
   204  
   205  func (cfg *InstanceConfig) ToolsDir(renderer shell.Renderer) string {
   206  	return cfg.agentInfo().ToolsDir(renderer)
   207  }
   208  
   209  func (cfg *InstanceConfig) InitService(renderer shell.Renderer) (service.Service, error) {
   210  	conf := service.AgentConf(cfg.agentInfo(), renderer)
   211  
   212  	name := cfg.MachineAgentServiceName
   213  	svc, err := newService(name, conf, cfg.Series)
   214  	return svc, errors.Trace(err)
   215  }
   216  
   217  var newService = func(name string, conf common.Conf, series string) (service.Service, error) {
   218  	return service.NewService(name, conf, series)
   219  }
   220  
   221  func (cfg *InstanceConfig) AgentConfig(
   222  	tag names.Tag,
   223  	toolsVersion version.Number,
   224  ) (agent.ConfigSetter, error) {
   225  	// TODO for HAState: the stateHostAddrs and apiHostAddrs here assume that
   226  	// if the instance is a controller then to use localhost.  This may be
   227  	// sufficient, but needs thought in the new world order.
   228  	var password string
   229  	if cfg.MongoInfo == nil {
   230  		password = cfg.APIInfo.Password
   231  	} else {
   232  		password = cfg.MongoInfo.Password
   233  	}
   234  	configParams := agent.AgentConfigParams{
   235  		Paths: agent.Paths{
   236  			DataDir:         cfg.DataDir,
   237  			LogDir:          cfg.LogDir,
   238  			MetricsSpoolDir: cfg.MetricsSpoolDir,
   239  		},
   240  		Jobs:              cfg.Jobs,
   241  		Tag:               tag,
   242  		UpgradedToVersion: toolsVersion,
   243  		Password:          password,
   244  		Nonce:             cfg.MachineNonce,
   245  		StateAddresses:    cfg.stateHostAddrs(),
   246  		APIAddresses:      cfg.ApiHostAddrs(),
   247  		CACert:            cfg.MongoInfo.CACert,
   248  		Values:            cfg.AgentEnvironment,
   249  		PreferIPv6:        cfg.PreferIPv6,
   250  		Model:             cfg.APIInfo.ModelTag,
   251  	}
   252  	if !cfg.Bootstrap {
   253  		return agent.NewAgentConfig(configParams)
   254  	}
   255  	return agent.NewStateMachineConfig(configParams, *cfg.StateServingInfo)
   256  }
   257  
   258  // JujuTools returns the directory where Juju tools are stored.
   259  func (cfg *InstanceConfig) JujuTools() string {
   260  	return agenttools.SharedToolsDir(cfg.DataDir, cfg.AgentVersion())
   261  }
   262  
   263  // GUITools returns the directory where the Juju GUI release is stored.
   264  func (cfg *InstanceConfig) GUITools() string {
   265  	return agenttools.SharedGUIDir(cfg.DataDir)
   266  }
   267  
   268  func (cfg *InstanceConfig) stateHostAddrs() []string {
   269  	var hosts []string
   270  	if cfg.Bootstrap {
   271  		if cfg.PreferIPv6 {
   272  			hosts = append(hosts, net.JoinHostPort("::1", strconv.Itoa(cfg.StateServingInfo.StatePort)))
   273  		} else {
   274  			hosts = append(hosts, net.JoinHostPort("localhost", strconv.Itoa(cfg.StateServingInfo.StatePort)))
   275  		}
   276  	}
   277  	if cfg.MongoInfo != nil {
   278  		hosts = append(hosts, cfg.MongoInfo.Addrs...)
   279  	}
   280  	return hosts
   281  }
   282  
   283  func (cfg *InstanceConfig) ApiHostAddrs() []string {
   284  	var hosts []string
   285  	if cfg.Bootstrap {
   286  		if cfg.PreferIPv6 {
   287  			hosts = append(hosts, net.JoinHostPort("::1", strconv.Itoa(cfg.StateServingInfo.APIPort)))
   288  		} else {
   289  			hosts = append(hosts, net.JoinHostPort("localhost", strconv.Itoa(cfg.StateServingInfo.APIPort)))
   290  		}
   291  	}
   292  	if cfg.APIInfo != nil {
   293  		hosts = append(hosts, cfg.APIInfo.Addrs...)
   294  	}
   295  	return hosts
   296  }
   297  
   298  // AgentVersion returns the version of the Juju agent that will be configured
   299  // on the instance. The zero value will be returned if there are no tools set.
   300  func (cfg *InstanceConfig) AgentVersion() version.Binary {
   301  	if len(cfg.tools) == 0 {
   302  		return version.Binary{}
   303  	}
   304  	return cfg.tools[0].Version
   305  }
   306  
   307  // ToolsList returns the list of tools in the order in which they will
   308  // be tried.
   309  func (cfg *InstanceConfig) ToolsList() coretools.List {
   310  	if cfg.tools == nil {
   311  		return nil
   312  	}
   313  	return copyToolsList(cfg.tools)
   314  }
   315  
   316  // SetTools sets the tools that should be tried when provisioning this
   317  // instance. There must be at least one. Other than the URL, each item
   318  // must be the same.
   319  //
   320  // TODO(axw) 2016-04-19 lp:1572116
   321  // SetTools should verify that the tools have URLs, since they will
   322  // be needed for downloading on the instance. We can't do that until
   323  // all usage-sites are updated to pass through non-empty URLs.
   324  func (cfg *InstanceConfig) SetTools(toolsList coretools.List) error {
   325  	if len(toolsList) == 0 {
   326  		return errors.New("need at least 1 tools")
   327  	}
   328  	var tools *coretools.Tools
   329  	for _, listed := range toolsList {
   330  		if listed == nil {
   331  			return errors.New("nil entry in tools list")
   332  		}
   333  		info := *listed
   334  		info.URL = ""
   335  		if tools == nil {
   336  			tools = &info
   337  			continue
   338  		}
   339  		if !reflect.DeepEqual(info, *tools) {
   340  			return errors.Errorf("tools info mismatch (%v, %v)", *tools, info)
   341  		}
   342  	}
   343  	cfg.tools = copyToolsList(toolsList)
   344  	return nil
   345  }
   346  
   347  func copyToolsList(in coretools.List) coretools.List {
   348  	out := make(coretools.List, len(in))
   349  	for i, tools := range in {
   350  		copied := *tools
   351  		out[i] = &copied
   352  	}
   353  	return out
   354  }
   355  
   356  type requiresError string
   357  
   358  func (e requiresError) Error() string {
   359  	return "invalid machine configuration: missing " + string(e)
   360  }
   361  
   362  func (cfg *InstanceConfig) VerifyConfig() (err error) {
   363  	defer errors.DeferredAnnotatef(&err, "invalid machine configuration")
   364  	if !names.IsValidMachine(cfg.MachineId) {
   365  		return errors.New("invalid machine id")
   366  	}
   367  	if cfg.DataDir == "" {
   368  		return errors.New("missing var directory")
   369  	}
   370  	if cfg.LogDir == "" {
   371  		return errors.New("missing log directory")
   372  	}
   373  	if cfg.MetricsSpoolDir == "" {
   374  		return errors.New("missing metrics spool directory")
   375  	}
   376  	if len(cfg.Jobs) == 0 {
   377  		return errors.New("missing machine jobs")
   378  	}
   379  	if cfg.CloudInitOutputLog == "" {
   380  		return errors.New("missing cloud-init output log path")
   381  	}
   382  	if cfg.tools == nil {
   383  		// SetTools() has never been called successfully.
   384  		return errors.New("missing tools")
   385  	}
   386  	// We don't need to check cfg.toolsURLs since SetTools() does.
   387  	if cfg.MongoInfo == nil {
   388  		return errors.New("missing state info")
   389  	}
   390  	if len(cfg.MongoInfo.CACert) == 0 {
   391  		return errors.New("missing CA certificate")
   392  	}
   393  	if cfg.APIInfo == nil {
   394  		return errors.New("missing API info")
   395  	}
   396  	if cfg.APIInfo.ModelTag.Id() == "" {
   397  		return errors.New("missing model tag")
   398  	}
   399  	if len(cfg.APIInfo.CACert) == 0 {
   400  		return errors.New("missing API CA certificate")
   401  	}
   402  	if cfg.MachineAgentServiceName == "" {
   403  		return errors.New("missing machine agent service name")
   404  	}
   405  	if cfg.Bootstrap {
   406  		if cfg.Config == nil {
   407  			return errors.New("missing model configuration")
   408  		}
   409  		if cfg.MongoInfo.Tag != nil {
   410  			return errors.New("entity tag must be nil when starting a controller")
   411  		}
   412  		if cfg.APIInfo.Tag != nil {
   413  			return errors.New("entity tag must be nil when starting a controller")
   414  		}
   415  		if cfg.StateServingInfo == nil {
   416  			return errors.New("missing state serving info")
   417  		}
   418  		if len(cfg.StateServingInfo.Cert) == 0 {
   419  			return errors.New("missing controller certificate")
   420  		}
   421  		if len(cfg.StateServingInfo.PrivateKey) == 0 {
   422  			return errors.New("missing controller private key")
   423  		}
   424  		if len(cfg.StateServingInfo.CAPrivateKey) == 0 {
   425  			return errors.New("missing ca cert private key")
   426  		}
   427  		if cfg.StateServingInfo.StatePort == 0 {
   428  			return errors.New("missing state port")
   429  		}
   430  		if cfg.StateServingInfo.APIPort == 0 {
   431  			return errors.New("missing API port")
   432  		}
   433  		if cfg.InstanceId == "" {
   434  			return errors.New("missing instance-id")
   435  		}
   436  		if len(cfg.HostedModelConfig) == 0 {
   437  			return errors.New("missing hosted model config")
   438  		}
   439  	} else {
   440  		if len(cfg.MongoInfo.Addrs) == 0 {
   441  			return errors.New("missing state hosts")
   442  		}
   443  		if cfg.MongoInfo.Tag != names.NewMachineTag(cfg.MachineId) {
   444  			return errors.New("entity tag must match started machine")
   445  		}
   446  		if len(cfg.APIInfo.Addrs) == 0 {
   447  			return errors.New("missing API hosts")
   448  		}
   449  		if cfg.APIInfo.Tag != names.NewMachineTag(cfg.MachineId) {
   450  			return errors.New("entity tag must match started machine")
   451  		}
   452  		if cfg.StateServingInfo != nil {
   453  			return errors.New("state serving info unexpectedly present")
   454  		}
   455  		if len(cfg.HostedModelConfig) != 0 {
   456  			return errors.New("hosted model config unexpectedly present")
   457  		}
   458  	}
   459  	if cfg.MachineNonce == "" {
   460  		return errors.New("missing machine nonce")
   461  	}
   462  	return nil
   463  }
   464  
   465  // DefaultBridgePrefix is the prefix for all network bridge device
   466  // name used for LXC and KVM containers.
   467  const DefaultBridgePrefix = "br-"
   468  
   469  // DefaultBridgeName is the network bridge device name used for LXC and KVM
   470  // containers
   471  const DefaultBridgeName = DefaultBridgePrefix + "eth0"
   472  
   473  // NewInstanceConfig sets up a basic machine configuration, for a
   474  // non-bootstrap node. You'll still need to supply more information,
   475  // but this takes care of the fixed entries and the ones that are
   476  // always needed.
   477  func NewInstanceConfig(
   478  	machineID,
   479  	machineNonce,
   480  	imageStream,
   481  	series,
   482  	publicImageSigningKey string,
   483  	secureServerConnections bool,
   484  	mongoInfo *mongo.MongoInfo,
   485  	apiInfo *api.Info,
   486  ) (*InstanceConfig, error) {
   487  	dataDir, err := paths.DataDir(series)
   488  	if err != nil {
   489  		return nil, err
   490  	}
   491  	logDir, err := paths.LogDir(series)
   492  	if err != nil {
   493  		return nil, err
   494  	}
   495  	metricsSpoolDir, err := paths.MetricsSpoolDir(series)
   496  	if err != nil {
   497  		return nil, err
   498  	}
   499  	cloudInitOutputLog := path.Join(logDir, "cloud-init-output.log")
   500  	icfg := &InstanceConfig{
   501  		// Fixed entries.
   502  		DataDir:                 dataDir,
   503  		LogDir:                  path.Join(logDir, "juju"),
   504  		MetricsSpoolDir:         metricsSpoolDir,
   505  		Jobs:                    []multiwatcher.MachineJob{multiwatcher.JobHostUnits},
   506  		CloudInitOutputLog:      cloudInitOutputLog,
   507  		MachineAgentServiceName: "jujud-" + names.NewMachineTag(machineID).String(),
   508  		Series:                  series,
   509  		Tags:                    map[string]string{},
   510  
   511  		// Parameter entries.
   512  		MachineId:             machineID,
   513  		MachineNonce:          machineNonce,
   514  		MongoInfo:             mongoInfo,
   515  		APIInfo:               apiInfo,
   516  		ImageStream:           imageStream,
   517  		PublicImageSigningKey: publicImageSigningKey,
   518  		AgentEnvironment: map[string]string{
   519  			agent.AllowsSecureConnection: strconv.FormatBool(secureServerConnections),
   520  		},
   521  	}
   522  	return icfg, nil
   523  }
   524  
   525  // NewBootstrapInstanceConfig sets up a basic machine configuration for a
   526  // bootstrap node.  You'll still need to supply more information, but this
   527  // takes care of the fixed entries and the ones that are always needed.
   528  func NewBootstrapInstanceConfig(
   529  	cons, modelCons constraints.Value,
   530  	series, publicImageSigningKey string,
   531  ) (*InstanceConfig, error) {
   532  	// For a bootstrap instance, FinishInstanceConfig will provide the
   533  	// state.Info and the api.Info. The machine id must *always* be "0".
   534  	icfg, err := NewInstanceConfig("0", agent.BootstrapNonce, "", series, publicImageSigningKey, true, nil, nil)
   535  	if err != nil {
   536  		return nil, err
   537  	}
   538  	icfg.Bootstrap = true
   539  	icfg.Jobs = []multiwatcher.MachineJob{
   540  		multiwatcher.JobManageModel,
   541  		multiwatcher.JobHostUnits,
   542  	}
   543  	icfg.Constraints = cons
   544  	icfg.ModelConstraints = modelCons
   545  	return icfg, nil
   546  }
   547  
   548  // PopulateInstanceConfig is called both from the FinishInstanceConfig below,
   549  // which does have access to the environment config, and from the container
   550  // provisioners, which don't have access to the environment config. Everything
   551  // that is needed to provision a container needs to be returned to the
   552  // provisioner in the ContainerConfig structure. Those values are then used to
   553  // call this function.
   554  func PopulateInstanceConfig(icfg *InstanceConfig,
   555  	providerType, authorizedKeys string,
   556  	sslHostnameVerification bool,
   557  	proxySettings, aptProxySettings proxy.Settings,
   558  	aptMirror string,
   559  	preferIPv6 bool,
   560  	enableOSRefreshUpdates bool,
   561  	enableOSUpgrade bool,
   562  ) error {
   563  	if authorizedKeys == "" {
   564  		return fmt.Errorf("model configuration has no authorized-keys")
   565  	}
   566  	icfg.AuthorizedKeys = authorizedKeys
   567  	if icfg.AgentEnvironment == nil {
   568  		icfg.AgentEnvironment = make(map[string]string)
   569  	}
   570  	icfg.AgentEnvironment[agent.ProviderType] = providerType
   571  	icfg.AgentEnvironment[agent.ContainerType] = string(icfg.MachineContainerType)
   572  	icfg.DisableSSLHostnameVerification = !sslHostnameVerification
   573  	icfg.ProxySettings = proxySettings
   574  	icfg.AptProxySettings = aptProxySettings
   575  	icfg.AptMirror = aptMirror
   576  	icfg.PreferIPv6 = preferIPv6
   577  	icfg.EnableOSRefreshUpdate = enableOSRefreshUpdates
   578  	icfg.EnableOSUpgrade = enableOSUpgrade
   579  	return nil
   580  }
   581  
   582  // FinishInstanceConfig sets fields on a InstanceConfig that can be determined by
   583  // inspecting a plain config.Config and the machine constraints at the last
   584  // moment before bootstrapping. It assumes that the supplied Config comes from
   585  // an environment that has passed through all the validation checks in the
   586  // Bootstrap func, and that has set an agent-version (via finding the tools to,
   587  // use for bootstrap, or otherwise).
   588  // TODO(fwereade) This function is not meant to be "good" in any serious way:
   589  // it is better that this functionality be collected in one place here than
   590  // that it be spread out across 3 or 4 providers, but this is its only
   591  // redeeming feature.
   592  func FinishInstanceConfig(icfg *InstanceConfig, cfg *config.Config) (err error) {
   593  	defer errors.DeferredAnnotatef(&err, "cannot complete machine configuration")
   594  
   595  	if err := PopulateInstanceConfig(
   596  		icfg,
   597  		cfg.Type(),
   598  		cfg.AuthorizedKeys(),
   599  		cfg.SSLHostnameVerification(),
   600  		cfg.ProxySettings(),
   601  		cfg.AptProxySettings(),
   602  		cfg.AptMirror(),
   603  		cfg.PreferIPv6(),
   604  		cfg.EnableOSRefreshUpdate(),
   605  		cfg.EnableOSUpgrade(),
   606  	); err != nil {
   607  		return errors.Trace(err)
   608  	}
   609  
   610  	if isStateInstanceConfig(icfg) {
   611  		// Add NUMACTL preference. Needed to work for both bootstrap and high availability
   612  		// Only makes sense for controller
   613  		logger.Debugf("Setting numa ctl preference to %v", cfg.NumaCtlPreference())
   614  		// Unfortunately, AgentEnvironment can only take strings as values
   615  		icfg.AgentEnvironment[agent.NumaCtlPreference] = fmt.Sprintf("%v", cfg.NumaCtlPreference())
   616  	}
   617  	// The following settings are only appropriate at bootstrap time. At the
   618  	// moment, the only controller is the bootstrap node, but this
   619  	// will probably change.
   620  	if !icfg.Bootstrap {
   621  		return nil
   622  	}
   623  	if icfg.APIInfo != nil || icfg.MongoInfo != nil {
   624  		return errors.New("machine configuration already has api/state info")
   625  	}
   626  	caCert, hasCACert := cfg.CACert()
   627  	if !hasCACert {
   628  		return errors.New("model configuration has no ca-cert")
   629  	}
   630  	password := cfg.AdminSecret()
   631  	if password == "" {
   632  		return errors.New("model configuration has no admin-secret")
   633  	}
   634  	icfg.APIInfo = &api.Info{
   635  		Password: password,
   636  		CACert:   caCert,
   637  		ModelTag: names.NewModelTag(cfg.UUID()),
   638  	}
   639  	icfg.MongoInfo = &mongo.MongoInfo{Password: password, Info: mongo.Info{CACert: caCert}}
   640  
   641  	// These really are directly relevant to running a controller.
   642  	// Initially, generate a controller certificate with no host IP
   643  	// addresses in the SAN field. Once the controller is up and the
   644  	// NIC addresses become known, the certificate can be regenerated.
   645  	cert, key, err := cfg.GenerateControllerCertAndKey(nil)
   646  	if err != nil {
   647  		return errors.Annotate(err, "cannot generate controller certificate")
   648  	}
   649  	caPrivateKey, hasCAPrivateKey := cfg.CAPrivateKey()
   650  	if !hasCAPrivateKey {
   651  		return errors.New("model configuration has no ca-private-key")
   652  	}
   653  	srvInfo := params.StateServingInfo{
   654  		StatePort:    cfg.StatePort(),
   655  		APIPort:      cfg.APIPort(),
   656  		Cert:         string(cert),
   657  		PrivateKey:   string(key),
   658  		CAPrivateKey: caPrivateKey,
   659  	}
   660  	icfg.StateServingInfo = &srvInfo
   661  	if icfg.Config, err = bootstrapConfig(cfg); err != nil {
   662  		return errors.Trace(err)
   663  	}
   664  
   665  	return nil
   666  }
   667  
   668  // InstanceTags returns the minimum set of tags that should be set on a
   669  // machine instance, if the provider supports them.
   670  func InstanceTags(cfg *config.Config, jobs []multiwatcher.MachineJob) map[string]string {
   671  	instanceTags := tags.ResourceTags(
   672  		names.NewModelTag(cfg.UUID()),
   673  		names.NewModelTag(cfg.ControllerUUID()),
   674  		cfg,
   675  	)
   676  	if multiwatcher.AnyJobNeedsState(jobs...) {
   677  		instanceTags[tags.JujuIsController] = "true"
   678  	}
   679  	return instanceTags
   680  }
   681  
   682  // bootstrapConfig returns a copy of the supplied configuration with the
   683  // admin-secret and ca-private-key attributes removed. If the resulting
   684  // config is not suitable for bootstrapping an environment, an error is
   685  // returned.
   686  // This function is copied from environs in here so we can avoid an import loop
   687  func bootstrapConfig(cfg *config.Config) (*config.Config, error) {
   688  	m := cfg.AllAttrs()
   689  	// We never want to push admin-secret or the root CA private key to the cloud.
   690  	delete(m, "admin-secret")
   691  	delete(m, "ca-private-key")
   692  	cfg, err := config.New(config.NoDefaults, m)
   693  	if err != nil {
   694  		return nil, err
   695  	}
   696  	if _, ok := cfg.AgentVersion(); !ok {
   697  		return nil, fmt.Errorf("model configuration has no agent-version")
   698  	}
   699  	return cfg, nil
   700  }
   701  
   702  // isStateInstanceConfig determines if given machine configuration
   703  // is for controller by iterating over machine's jobs.
   704  // If JobManageModel is present, this is a controller.
   705  func isStateInstanceConfig(icfg *InstanceConfig) bool {
   706  	for _, aJob := range icfg.Jobs {
   707  		if aJob == multiwatcher.JobManageModel {
   708  			return true
   709  		}
   710  	}
   711  	return false
   712  }