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