github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/jujud/bootstrap.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"net"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/juju/cmd"
    17  	"github.com/juju/errors"
    18  	"github.com/juju/gnuflag"
    19  	"github.com/juju/loggo"
    20  	"github.com/juju/os/series"
    21  	"github.com/juju/utils/arch"
    22  	"github.com/juju/utils/ssh"
    23  	"github.com/juju/version"
    24  	"gopkg.in/juju/names.v2"
    25  
    26  	"github.com/juju/juju/agent"
    27  	"github.com/juju/juju/agent/agentbootstrap"
    28  	agenttools "github.com/juju/juju/agent/tools"
    29  	"github.com/juju/juju/cloudconfig/instancecfg"
    30  	jujucmd "github.com/juju/juju/cmd"
    31  	agentcmd "github.com/juju/juju/cmd/jujud/agent"
    32  	cmdutil "github.com/juju/juju/cmd/jujud/util"
    33  	"github.com/juju/juju/core/instance"
    34  	"github.com/juju/juju/environs"
    35  	"github.com/juju/juju/environs/config"
    36  	"github.com/juju/juju/environs/context"
    37  	"github.com/juju/juju/environs/imagemetadata"
    38  	"github.com/juju/juju/environs/simplestreams"
    39  	envtools "github.com/juju/juju/environs/tools"
    40  	"github.com/juju/juju/mongo"
    41  	"github.com/juju/juju/network"
    42  	"github.com/juju/juju/state"
    43  	"github.com/juju/juju/state/binarystorage"
    44  	"github.com/juju/juju/state/cloudimagemetadata"
    45  	"github.com/juju/juju/state/multiwatcher"
    46  	"github.com/juju/juju/state/stateenvirons"
    47  	"github.com/juju/juju/tools"
    48  	jujuversion "github.com/juju/juju/version"
    49  	"github.com/juju/juju/worker/peergrouper"
    50  )
    51  
    52  var (
    53  	initiateMongoServer  = peergrouper.InitiateMongoServer
    54  	agentInitializeState = agentbootstrap.InitializeState
    55  	sshGenerateKey       = ssh.GenerateKey
    56  	minSocketTimeout     = 1 * time.Minute
    57  	logger               = loggo.GetLogger("juju.cmd.jujud")
    58  )
    59  
    60  const adminUserName = "admin"
    61  
    62  // BootstrapCommand represents a jujud bootstrap command.
    63  type BootstrapCommand struct {
    64  	cmd.CommandBase
    65  	agentcmd.AgentConf
    66  	BootstrapParamsFile string
    67  	Timeout             time.Duration
    68  }
    69  
    70  // NewBootstrapCommand returns a new BootstrapCommand that has been initialized.
    71  func NewBootstrapCommand() *BootstrapCommand {
    72  	return &BootstrapCommand{
    73  		AgentConf: agentcmd.NewAgentConf(""),
    74  	}
    75  }
    76  
    77  // Info returns a description of the command.
    78  func (c *BootstrapCommand) Info() *cmd.Info {
    79  	return jujucmd.Info(&cmd.Info{
    80  		Name:    "bootstrap-state",
    81  		Purpose: "initialize juju state",
    82  	})
    83  }
    84  
    85  // SetFlags adds the flags for this command to the passed gnuflag.FlagSet.
    86  func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
    87  	c.AgentConf.AddFlags(f)
    88  	f.DurationVar(&c.Timeout, "timeout", time.Duration(0), "set the bootstrap timeout")
    89  }
    90  
    91  // Init initializes the command for running.
    92  func (c *BootstrapCommand) Init(args []string) error {
    93  	if len(args) == 0 {
    94  		return errors.New("bootstrap-params file must be specified")
    95  	}
    96  	if err := cmd.CheckEmpty(args[1:]); err != nil {
    97  		return err
    98  	}
    99  	c.BootstrapParamsFile = args[0]
   100  	return c.AgentConf.CheckArgs(args[1:])
   101  }
   102  
   103  // EnvironsNew defines function used to get reference to an underlying cloud provider.
   104  var EnvironsNew = environs.New
   105  
   106  // Run initializes state for an environment.
   107  func (c *BootstrapCommand) Run(_ *cmd.Context) error {
   108  	bootstrapParamsData, err := ioutil.ReadFile(c.BootstrapParamsFile)
   109  	if err != nil {
   110  		return errors.Annotate(err, "reading bootstrap params file")
   111  	}
   112  	var args instancecfg.StateInitializationParams
   113  	if err := args.Unmarshal(bootstrapParamsData); err != nil {
   114  		return errors.Trace(err)
   115  	}
   116  
   117  	err = c.ReadConfig("machine-0")
   118  	if err != nil {
   119  		return errors.Annotate(err, "cannot read config")
   120  	}
   121  	agentConfig := c.CurrentConfig()
   122  
   123  	// agent.Jobs is an optional field in the agent config, and was
   124  	// introduced after 1.17.2. We default to allowing units on
   125  	// machine-0 if missing.
   126  	jobs := agentConfig.Jobs()
   127  	if len(jobs) == 0 {
   128  		jobs = []multiwatcher.MachineJob{
   129  			multiwatcher.JobManageModel,
   130  			multiwatcher.JobHostUnits,
   131  		}
   132  	}
   133  
   134  	// Get the bootstrap machine's addresses from the provider.
   135  	cloudSpec, err := environs.MakeCloudSpec(
   136  		args.ControllerCloud,
   137  		args.ControllerCloudRegion,
   138  		args.ControllerCloudCredential,
   139  	)
   140  	if err != nil {
   141  		return errors.Trace(err)
   142  	}
   143  	env, err := EnvironsNew(environs.OpenParams{
   144  		Cloud:  cloudSpec,
   145  		Config: args.ControllerModelConfig,
   146  	})
   147  	if err != nil {
   148  		return errors.Annotate(err, "new environ")
   149  	}
   150  	newConfigAttrs := make(map[string]interface{})
   151  
   152  	// Check to see if a newer agent version has been requested
   153  	// by the bootstrap client.
   154  	desiredVersion, ok := args.ControllerModelConfig.AgentVersion()
   155  	if ok && desiredVersion != jujuversion.Current {
   156  		// If we have been asked for a newer version, ensure the newer
   157  		// tools can actually be found, or else bootstrap won't complete.
   158  		streams := envtools.PreferredStreams(&desiredVersion, args.ControllerModelConfig.Development(), args.ControllerModelConfig.AgentStream())
   159  		logger.Infof("newer agent binaries requested, looking for %v in streams: %v", desiredVersion, strings.Join(streams, ","))
   160  		hostSeries, err := series.HostSeries()
   161  		if err != nil {
   162  			return errors.Trace(err)
   163  		}
   164  		filter := tools.Filter{
   165  			Number: desiredVersion,
   166  			Arch:   arch.HostArch(),
   167  			Series: hostSeries,
   168  		}
   169  		_, toolsErr := envtools.FindTools(env, -1, -1, streams, filter)
   170  		if toolsErr == nil {
   171  			logger.Infof("agent binaries are available, upgrade will occur after bootstrap")
   172  		}
   173  		if errors.IsNotFound(toolsErr) {
   174  			// Newer tools not available, so revert to using the tools
   175  			// matching the current agent version.
   176  			logger.Warningf("newer agent binaries for %q not available, sticking with version %q", desiredVersion, jujuversion.Current)
   177  			newConfigAttrs["agent-version"] = jujuversion.Current.String()
   178  		} else if toolsErr != nil {
   179  			logger.Errorf("cannot find newer agent binaries: %v", toolsErr)
   180  			return toolsErr
   181  		}
   182  	}
   183  
   184  	callCtx := context.NewCloudCallContext()
   185  	// At this stage, cloud credential has not yet been stored server-side
   186  	// as there is no server-side. If these cloud calls will fail with
   187  	// invalid credential, just log it.
   188  	callCtx.InvalidateCredentialFunc = func(reason string) error {
   189  		logger.Errorf("Cloud credential %q is not accepted by cloud provider: %v", args.ControllerCloudCredentialName, reason)
   190  		return nil
   191  	}
   192  
   193  	instances, err := env.Instances(callCtx, []instance.Id{args.BootstrapMachineInstanceId})
   194  	if err != nil {
   195  		return errors.Annotate(err, "getting bootstrap instance")
   196  	}
   197  	addrs, err := instances[0].Addresses(callCtx)
   198  	if err != nil {
   199  		return errors.Annotate(err, "bootstrap instance addresses")
   200  	}
   201  
   202  	// When machine addresses are reported from state, they have
   203  	// duplicates removed.  We should do the same here so that
   204  	// there is not unnecessary churn in the mongo replicaset.
   205  	// TODO (cherylj) Add explicit unit tests for this - tracked
   206  	// by bug #1544158.
   207  	addrs = network.MergedAddresses([]network.Address{}, addrs)
   208  
   209  	// Generate a private SSH key for the controllers, and add
   210  	// the public key to the environment config. We'll add the
   211  	// private key to StateServingInfo below.
   212  	privateKey, publicKey, err := sshGenerateKey(config.JujuSystemKey)
   213  	if err != nil {
   214  		return errors.Annotate(err, "failed to generate system key")
   215  	}
   216  	authorizedKeys := config.ConcatAuthKeys(args.ControllerModelConfig.AuthorizedKeys(), publicKey)
   217  	newConfigAttrs[config.AuthorizedKeysKey] = authorizedKeys
   218  
   219  	// Generate a shared secret for the Mongo replica set, and write it out.
   220  	sharedSecret, err := mongo.GenerateSharedSecret()
   221  	if err != nil {
   222  		return err
   223  	}
   224  	info, ok := agentConfig.StateServingInfo()
   225  	if !ok {
   226  		return fmt.Errorf("bootstrap machine config has no state serving info")
   227  	}
   228  	info.SharedSecret = sharedSecret
   229  	info.SystemIdentity = privateKey
   230  	err = c.ChangeConfig(func(agentConfig agent.ConfigSetter) error {
   231  		agentConfig.SetStateServingInfo(info)
   232  		mmprof, err := mongo.NewMemoryProfile(args.ControllerConfig.MongoMemoryProfile())
   233  		if err != nil {
   234  			logger.Errorf("could not set requested memory profile: %v", err)
   235  		} else {
   236  			agentConfig.SetMongoMemoryProfile(mmprof)
   237  		}
   238  		return nil
   239  	})
   240  	if err != nil {
   241  		return fmt.Errorf("cannot write agent config: %v", err)
   242  	}
   243  
   244  	agentConfig = c.CurrentConfig()
   245  
   246  	// Create system-identity file
   247  	if err := agent.WriteSystemIdentityFile(agentConfig); err != nil {
   248  		return err
   249  	}
   250  
   251  	if err := c.startMongo(addrs, agentConfig); err != nil {
   252  		return errors.Annotate(err, "failed to start mongo")
   253  	}
   254  
   255  	controllerModelCfg, err := env.Config().Apply(newConfigAttrs)
   256  	if err != nil {
   257  		return errors.Annotate(err, "failed to update model config")
   258  	}
   259  	args.ControllerModelConfig = controllerModelCfg
   260  
   261  	// Initialise state, and store any agent config (e.g. password) changes.
   262  	var controller *state.Controller
   263  	var m *state.Machine
   264  	err = c.ChangeConfig(func(agentConfig agent.ConfigSetter) error {
   265  		var stateErr error
   266  		dialOpts := mongo.DefaultDialOpts()
   267  
   268  		// Set a longer socket timeout than usual, as the machine
   269  		// will be starting up and disk I/O slower than usual. This
   270  		// has been known to cause timeouts in queries.
   271  		dialOpts.SocketTimeout = c.Timeout
   272  		if dialOpts.SocketTimeout < minSocketTimeout {
   273  			dialOpts.SocketTimeout = minSocketTimeout
   274  		}
   275  
   276  		// We shouldn't attempt to dial peers until we have some.
   277  		dialOpts.Direct = true
   278  
   279  		adminTag := names.NewLocalUserTag(adminUserName)
   280  		controller, m, stateErr = agentInitializeState(
   281  			adminTag,
   282  			agentConfig,
   283  			agentbootstrap.InitializeStateParams{
   284  				StateInitializationParams: args,
   285  				BootstrapMachineAddresses: addrs,
   286  				BootstrapMachineJobs:      jobs,
   287  				SharedSecret:              sharedSecret,
   288  				Provider:                  environs.Provider,
   289  				StorageProviderRegistry:   stateenvirons.NewStorageProviderRegistry(env),
   290  			},
   291  			dialOpts,
   292  			stateenvirons.GetNewPolicyFunc(),
   293  		)
   294  		return stateErr
   295  	})
   296  	if err != nil {
   297  		return err
   298  	}
   299  	defer controller.Close()
   300  	st := controller.SystemState()
   301  
   302  	// Set up default container networking mode
   303  	model, err := st.Model()
   304  	if err != nil {
   305  		return err
   306  	}
   307  	if err = model.AutoConfigureContainerNetworking(env); err != nil {
   308  		if errors.IsNotSupported(err) {
   309  			logger.Debugf("Not performing container networking autoconfiguration on a non-networking environment")
   310  		} else {
   311  			return err
   312  		}
   313  	}
   314  
   315  	// Fetch spaces from substrate
   316  	if err = st.ReloadSpaces(env); err != nil {
   317  		if errors.IsNotSupported(err) {
   318  			logger.Debugf("Not performing spaces load on a non-networking environment")
   319  		} else {
   320  			return err
   321  		}
   322  	}
   323  
   324  	// Populate the tools catalogue.
   325  	if err := c.populateTools(st, env); err != nil {
   326  		return err
   327  	}
   328  
   329  	// Populate the GUI archive catalogue.
   330  	if err := c.populateGUIArchive(st, env); err != nil {
   331  		// Do not stop the bootstrapping process for Juju GUI archive errors.
   332  		logger.Warningf("cannot set up Juju GUI: %s", err)
   333  	} else {
   334  		logger.Debugf("Juju GUI successfully set up")
   335  	}
   336  
   337  	// Add custom image metadata to environment storage.
   338  	if len(args.CustomImageMetadata) > 0 {
   339  		if err := c.saveCustomImageMetadata(st, env, args.CustomImageMetadata); err != nil {
   340  			return err
   341  		}
   342  	}
   343  
   344  	// bootstrap machine always gets the vote
   345  	return m.SetHasVote(true)
   346  }
   347  
   348  func (c *BootstrapCommand) startMongo(addrs []network.Address, agentConfig agent.Config) error {
   349  	logger.Debugf("starting mongo")
   350  
   351  	info, ok := agentConfig.MongoInfo()
   352  	if !ok {
   353  		return fmt.Errorf("no state info available")
   354  	}
   355  	// When bootstrapping, we need to allow enough time for mongo
   356  	// to start as there's no retry loop in place.
   357  	// 5 minutes should suffice.
   358  	mongoDialOpts := mongo.DialOpts{Timeout: 5 * time.Minute}
   359  	dialInfo, err := mongo.DialInfo(info.Info, mongoDialOpts)
   360  	if err != nil {
   361  		return err
   362  	}
   363  	servingInfo, ok := agentConfig.StateServingInfo()
   364  	if !ok {
   365  		return fmt.Errorf("agent config has no state serving info")
   366  	}
   367  	// Use localhost to dial the mongo server, because it's running in
   368  	// auth mode and will refuse to perform any operations unless
   369  	// we dial that address.
   370  	// TODO(macgreagoir) IPv6. Ubuntu still always provides IPv4 loopback,
   371  	// and when/if this changes localhost should resolve to IPv6 loopback
   372  	// in any case (lp:1644009). Review.
   373  	dialInfo.Addrs = []string{
   374  		net.JoinHostPort("localhost", fmt.Sprint(servingInfo.StatePort)),
   375  	}
   376  
   377  	logger.Debugf("calling ensureMongoServer")
   378  	ensureServerParams, err := cmdutil.NewEnsureServerParams(agentConfig)
   379  	if err != nil {
   380  		return err
   381  	}
   382  	_, err = cmdutil.EnsureMongoServer(ensureServerParams)
   383  	if err != nil {
   384  		return err
   385  	}
   386  
   387  	peerAddr := mongo.SelectPeerAddress(addrs)
   388  	if peerAddr == "" {
   389  		return fmt.Errorf("no appropriate peer address found in %q", addrs)
   390  	}
   391  	peerHostPort := net.JoinHostPort(peerAddr, fmt.Sprint(servingInfo.StatePort))
   392  
   393  	if err := initiateMongoServer(peergrouper.InitiateMongoParams{
   394  		DialInfo:       dialInfo,
   395  		MemberHostPort: peerHostPort,
   396  	}); err != nil {
   397  		return err
   398  	}
   399  	logger.Infof("started mongo")
   400  	return nil
   401  }
   402  
   403  // populateTools stores uploaded tools in provider storage
   404  // and updates the tools metadata.
   405  func (c *BootstrapCommand) populateTools(st *state.State, env environs.Environ) error {
   406  	agentConfig := c.CurrentConfig()
   407  	dataDir := agentConfig.DataDir()
   408  
   409  	hostSeries, err := series.HostSeries()
   410  	if err != nil {
   411  		return errors.Trace(err)
   412  	}
   413  	current := version.Binary{
   414  		Number: jujuversion.Current,
   415  		Arch:   arch.HostArch(),
   416  		Series: hostSeries,
   417  	}
   418  	tools, err := agenttools.ReadTools(dataDir, current)
   419  	if err != nil {
   420  		return errors.Trace(err)
   421  	}
   422  
   423  	data, err := ioutil.ReadFile(filepath.Join(
   424  		agenttools.SharedToolsDir(dataDir, current),
   425  		"tools.tar.gz",
   426  	))
   427  	if err != nil {
   428  		return errors.Trace(err)
   429  	}
   430  
   431  	toolstorage, err := st.ToolsStorage()
   432  	if err != nil {
   433  		return errors.Trace(err)
   434  	}
   435  	defer toolstorage.Close()
   436  
   437  	var toolsVersions []version.Binary
   438  	if strings.HasPrefix(tools.URL, "file://") {
   439  		// Tools were uploaded: clone for each series of the same OS.
   440  		os, err := series.GetOSFromSeries(tools.Version.Series)
   441  		if err != nil {
   442  			return errors.Trace(err)
   443  		}
   444  		osSeries := series.OSSupportedSeries(os)
   445  		for _, series := range osSeries {
   446  			toolsVersion := tools.Version
   447  			toolsVersion.Series = series
   448  			toolsVersions = append(toolsVersions, toolsVersion)
   449  		}
   450  	} else {
   451  		// Tools were downloaded from an external source: don't clone.
   452  		toolsVersions = []version.Binary{tools.Version}
   453  	}
   454  
   455  	for _, toolsVersion := range toolsVersions {
   456  		metadata := binarystorage.Metadata{
   457  			Version: toolsVersion.String(),
   458  			Size:    tools.Size,
   459  			SHA256:  tools.SHA256,
   460  		}
   461  		logger.Debugf("Adding agent binaries: %v", toolsVersion)
   462  		if err := toolstorage.Add(bytes.NewReader(data), metadata); err != nil {
   463  			return errors.Trace(err)
   464  		}
   465  	}
   466  	return nil
   467  }
   468  
   469  // populateGUIArchive stores the uploaded Juju GUI archive in provider storage,
   470  // updates the GUI metadata and set the current Juju GUI version.
   471  func (c *BootstrapCommand) populateGUIArchive(st *state.State, env environs.Environ) error {
   472  	agentConfig := c.CurrentConfig()
   473  	dataDir := agentConfig.DataDir()
   474  	guistorage, err := st.GUIStorage()
   475  	if err != nil {
   476  		return errors.Trace(err)
   477  	}
   478  	defer guistorage.Close()
   479  	gui, err := agenttools.ReadGUIArchive(dataDir)
   480  	if err != nil {
   481  		return errors.Annotate(err, "cannot fetch GUI info")
   482  	}
   483  	f, err := os.Open(filepath.Join(agenttools.SharedGUIDir(dataDir), "gui.tar.bz2"))
   484  	if err != nil {
   485  		return errors.Annotate(err, "cannot read GUI archive")
   486  	}
   487  	defer f.Close()
   488  	if err := guistorage.Add(f, binarystorage.Metadata{
   489  		Version: gui.Version.String(),
   490  		Size:    gui.Size,
   491  		SHA256:  gui.SHA256,
   492  	}); err != nil {
   493  		return errors.Annotate(err, "cannot store GUI archive")
   494  	}
   495  	if err = st.GUISetVersion(gui.Version); err != nil {
   496  		return errors.Annotate(err, "cannot set current GUI version")
   497  	}
   498  	return nil
   499  }
   500  
   501  // Override for testing.
   502  var seriesFromVersion = series.VersionSeries
   503  
   504  // saveCustomImageMetadata stores the custom image metadata to the database,
   505  func (c *BootstrapCommand) saveCustomImageMetadata(st *state.State, env environs.Environ, imageMetadata []*imagemetadata.ImageMetadata) error {
   506  	logger.Debugf("saving custom image metadata")
   507  	return storeImageMetadataInState(st, env, "custom", simplestreams.CUSTOM_CLOUD_DATA, imageMetadata)
   508  }
   509  
   510  // storeImageMetadataInState writes image metadata into state store.
   511  func storeImageMetadataInState(st *state.State, env environs.Environ, source string, priority int, existingMetadata []*imagemetadata.ImageMetadata) error {
   512  	if len(existingMetadata) == 0 {
   513  		return nil
   514  	}
   515  	cfg := env.Config()
   516  	metadataState := make([]cloudimagemetadata.Metadata, len(existingMetadata))
   517  	for i, one := range existingMetadata {
   518  		m := cloudimagemetadata.Metadata{
   519  			MetadataAttributes: cloudimagemetadata.MetadataAttributes{
   520  				Stream:          one.Stream,
   521  				Region:          one.RegionName,
   522  				Arch:            one.Arch,
   523  				VirtType:        one.VirtType,
   524  				RootStorageType: one.Storage,
   525  				Source:          source,
   526  				Version:         one.Version,
   527  			},
   528  			Priority: priority,
   529  			ImageId:  one.Id,
   530  		}
   531  		s, err := seriesFromVersion(one.Version)
   532  		if err != nil {
   533  			return errors.Annotatef(err, "cannot determine series for version %v", one.Version)
   534  		}
   535  		m.Series = s
   536  		if m.Stream == "" {
   537  			m.Stream = cfg.ImageStream()
   538  		}
   539  		if m.Source == "" {
   540  			m.Source = "custom"
   541  		}
   542  		metadataState[i] = m
   543  	}
   544  	if err := st.CloudImageMetadataStorage.SaveMetadataNoExpiry(metadataState); err != nil {
   545  		return errors.Annotatef(err, "cannot cache image metadata")
   546  	}
   547  	return nil
   548  }