github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/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  	"encoding/base64"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"net"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/juju/cmd"
    18  	"github.com/juju/errors"
    19  	"github.com/juju/loggo"
    20  	"github.com/juju/names"
    21  	"github.com/juju/utils"
    22  	"github.com/juju/utils/series"
    23  	goyaml "gopkg.in/yaml.v1"
    24  	"launchpad.net/gnuflag"
    25  
    26  	"github.com/juju/juju/agent"
    27  	agenttools "github.com/juju/juju/agent/tools"
    28  	agentcmd "github.com/juju/juju/cmd/jujud/agent"
    29  	cmdutil "github.com/juju/juju/cmd/jujud/util"
    30  	"github.com/juju/juju/constraints"
    31  	"github.com/juju/juju/environs"
    32  	"github.com/juju/juju/environs/config"
    33  	"github.com/juju/juju/environs/imagemetadata"
    34  	"github.com/juju/juju/environs/simplestreams"
    35  	"github.com/juju/juju/instance"
    36  	"github.com/juju/juju/mongo"
    37  	"github.com/juju/juju/network"
    38  	"github.com/juju/juju/state"
    39  	"github.com/juju/juju/state/cloudimagemetadata"
    40  	"github.com/juju/juju/state/multiwatcher"
    41  	"github.com/juju/juju/state/storage"
    42  	"github.com/juju/juju/state/toolstorage"
    43  	"github.com/juju/juju/storage/poolmanager"
    44  	"github.com/juju/juju/utils/ssh"
    45  	"github.com/juju/juju/version"
    46  	"github.com/juju/juju/worker/peergrouper"
    47  )
    48  
    49  var (
    50  	maybeInitiateMongoServer = peergrouper.MaybeInitiateMongoServer
    51  	agentInitializeState     = agent.InitializeState
    52  	sshGenerateKey           = ssh.GenerateKey
    53  	newStateStorage          = storage.NewStorage
    54  	minSocketTimeout         = 1 * time.Minute
    55  	logger                   = loggo.GetLogger("juju.cmd.jujud")
    56  )
    57  
    58  type BootstrapCommand struct {
    59  	cmd.CommandBase
    60  	agentcmd.AgentConf
    61  	EnvConfig        map[string]interface{}
    62  	Constraints      constraints.Value
    63  	Hardware         instance.HardwareCharacteristics
    64  	InstanceId       string
    65  	AdminUsername    string
    66  	ImageMetadataDir string
    67  }
    68  
    69  // NewBootstrapCommand returns a new BootstrapCommand that has been initialized.
    70  func NewBootstrapCommand() *BootstrapCommand {
    71  	return &BootstrapCommand{
    72  		AgentConf: agentcmd.NewAgentConf(""),
    73  	}
    74  }
    75  
    76  // Info returns a decription of the command.
    77  func (c *BootstrapCommand) Info() *cmd.Info {
    78  	return &cmd.Info{
    79  		Name:    "bootstrap-state",
    80  		Purpose: "initialize juju state",
    81  	}
    82  }
    83  
    84  func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
    85  	c.AgentConf.AddFlags(f)
    86  	yamlBase64Var(f, &c.EnvConfig, "env-config", "", "initial environment configuration (yaml, base64 encoded)")
    87  	f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "initial environment constraints (space-separated strings)")
    88  	f.Var(&c.Hardware, "hardware", "hardware characteristics (space-separated strings)")
    89  	f.StringVar(&c.InstanceId, "instance-id", "", "unique instance-id for bootstrap machine")
    90  	f.StringVar(&c.AdminUsername, "admin-user", "admin", "set the name for the juju admin user")
    91  	f.StringVar(&c.ImageMetadataDir, "image-metadata", "", "custom image metadata source dir")
    92  }
    93  
    94  // Init initializes the command for running.
    95  func (c *BootstrapCommand) Init(args []string) error {
    96  	if len(c.EnvConfig) == 0 {
    97  		return cmdutil.RequiredError("env-config")
    98  	}
    99  	if c.InstanceId == "" {
   100  		return cmdutil.RequiredError("instance-id")
   101  	}
   102  	if !names.IsValidUser(c.AdminUsername) {
   103  		return errors.Errorf("%q is not a valid username", c.AdminUsername)
   104  	}
   105  	return c.AgentConf.CheckArgs(args)
   106  }
   107  
   108  // Run initializes state for an environment.
   109  func (c *BootstrapCommand) Run(_ *cmd.Context) error {
   110  	envCfg, err := config.New(config.NoDefaults, c.EnvConfig)
   111  	if err != nil {
   112  		return err
   113  	}
   114  	err = c.ReadConfig("machine-0")
   115  	if err != nil {
   116  		return err
   117  	}
   118  	agentConfig := c.CurrentConfig()
   119  	network.InitializeFromConfig(agentConfig)
   120  
   121  	// agent.Jobs is an optional field in the agent config, and was
   122  	// introduced after 1.17.2. We default to allowing units on
   123  	// machine-0 if missing.
   124  	jobs := agentConfig.Jobs()
   125  	if len(jobs) == 0 {
   126  		jobs = []multiwatcher.MachineJob{
   127  			multiwatcher.JobManageEnviron,
   128  			multiwatcher.JobHostUnits,
   129  			multiwatcher.JobManageNetworking,
   130  		}
   131  	}
   132  
   133  	// Get the bootstrap machine's addresses from the provider.
   134  	env, err := environs.New(envCfg)
   135  	if err != nil {
   136  		return err
   137  	}
   138  	instanceId := instance.Id(c.InstanceId)
   139  	instances, err := env.Instances([]instance.Id{instanceId})
   140  	if err != nil {
   141  		return err
   142  	}
   143  	addrs, err := instances[0].Addresses()
   144  	if err != nil {
   145  		return err
   146  	}
   147  
   148  	// Generate a private SSH key for the state servers, and add
   149  	// the public key to the environment config. We'll add the
   150  	// private key to StateServingInfo below.
   151  	privateKey, publicKey, err := sshGenerateKey(config.JujuSystemKey)
   152  	if err != nil {
   153  		return errors.Annotate(err, "failed to generate system key")
   154  	}
   155  	authorizedKeys := config.ConcatAuthKeys(envCfg.AuthorizedKeys(), publicKey)
   156  	envCfg, err = env.Config().Apply(map[string]interface{}{
   157  		config.AuthKeysConfig: authorizedKeys,
   158  	})
   159  	if err != nil {
   160  		return errors.Annotate(err, "failed to add public key to environment config")
   161  	}
   162  
   163  	// Generate a shared secret for the Mongo replica set, and write it out.
   164  	sharedSecret, err := mongo.GenerateSharedSecret()
   165  	if err != nil {
   166  		return err
   167  	}
   168  	info, ok := agentConfig.StateServingInfo()
   169  	if !ok {
   170  		return fmt.Errorf("bootstrap machine config has no state serving info")
   171  	}
   172  	info.SharedSecret = sharedSecret
   173  	info.SystemIdentity = privateKey
   174  	err = c.ChangeConfig(func(agentConfig agent.ConfigSetter) error {
   175  		agentConfig.SetStateServingInfo(info)
   176  		return nil
   177  	})
   178  	if err != nil {
   179  		return fmt.Errorf("cannot write agent config: %v", err)
   180  	}
   181  	agentConfig = c.CurrentConfig()
   182  
   183  	// Create system-identity file
   184  	if err := agent.WriteSystemIdentityFile(agentConfig); err != nil {
   185  		return err
   186  	}
   187  
   188  	if err := c.startMongo(addrs, agentConfig); err != nil {
   189  		return err
   190  	}
   191  
   192  	logger.Infof("started mongo")
   193  	// Initialise state, and store any agent config (e.g. password) changes.
   194  	var st *state.State
   195  	var m *state.Machine
   196  	err = c.ChangeConfig(func(agentConfig agent.ConfigSetter) error {
   197  		var stateErr error
   198  		dialOpts := mongo.DefaultDialOpts()
   199  
   200  		// Set a longer socket timeout than usual, as the machine
   201  		// will be starting up and disk I/O slower than usual. This
   202  		// has been known to cause timeouts in queries.
   203  		timeouts := envCfg.BootstrapSSHOpts()
   204  		dialOpts.SocketTimeout = timeouts.Timeout
   205  		if dialOpts.SocketTimeout < minSocketTimeout {
   206  			dialOpts.SocketTimeout = minSocketTimeout
   207  		}
   208  
   209  		// We shouldn't attempt to dial peers until we have some.
   210  		dialOpts.Direct = true
   211  
   212  		adminTag := names.NewLocalUserTag(c.AdminUsername)
   213  		st, m, stateErr = agentInitializeState(
   214  			adminTag,
   215  			agentConfig,
   216  			envCfg,
   217  			agent.BootstrapMachineConfig{
   218  				Addresses:       addrs,
   219  				Constraints:     c.Constraints,
   220  				Jobs:            jobs,
   221  				InstanceId:      instanceId,
   222  				Characteristics: c.Hardware,
   223  				SharedSecret:    sharedSecret,
   224  			},
   225  			dialOpts,
   226  			environs.NewStatePolicy(),
   227  		)
   228  		return stateErr
   229  	})
   230  	if err != nil {
   231  		return err
   232  	}
   233  	defer st.Close()
   234  
   235  	// Populate the tools catalogue.
   236  	if err := c.populateTools(st, env); err != nil {
   237  		return err
   238  	}
   239  
   240  	// Add custom image metadata to environment storage.
   241  	if c.ImageMetadataDir != "" {
   242  		if err := c.saveCustomImageMetadata(st); err != nil {
   243  			return err
   244  		}
   245  
   246  		// TODO (anastasiamac 2015-09-24) Remove this once search path is updated..
   247  		stor := newStateStorage(st.EnvironUUID(), st.MongoSession())
   248  		if err := c.storeCustomImageMetadata(stor); err != nil {
   249  			return err
   250  		}
   251  	}
   252  
   253  	// Populate the storage pools.
   254  	if err := c.populateDefaultStoragePools(st); err != nil {
   255  		return err
   256  	}
   257  
   258  	// bootstrap machine always gets the vote
   259  	return m.SetHasVote(true)
   260  }
   261  
   262  func (c *BootstrapCommand) startMongo(addrs []network.Address, agentConfig agent.Config) error {
   263  	logger.Debugf("starting mongo")
   264  
   265  	info, ok := agentConfig.MongoInfo()
   266  	if !ok {
   267  		return fmt.Errorf("no state info available")
   268  	}
   269  	// When bootstrapping, we need to allow enough time for mongo
   270  	// to start as there's no retry loop in place.
   271  	// 5 minutes should suffice.
   272  	bootstrapDialOpts := mongo.DialOpts{Timeout: 5 * time.Minute}
   273  	dialInfo, err := mongo.DialInfo(info.Info, bootstrapDialOpts)
   274  	if err != nil {
   275  		return err
   276  	}
   277  	servingInfo, ok := agentConfig.StateServingInfo()
   278  	if !ok {
   279  		return fmt.Errorf("agent config has no state serving info")
   280  	}
   281  	// Use localhost to dial the mongo server, because it's running in
   282  	// auth mode and will refuse to perform any operations unless
   283  	// we dial that address.
   284  	dialInfo.Addrs = []string{
   285  		net.JoinHostPort("127.0.0.1", fmt.Sprint(servingInfo.StatePort)),
   286  	}
   287  
   288  	logger.Debugf("calling ensureMongoServer")
   289  	ensureServerParams, err := cmdutil.NewEnsureServerParams(agentConfig)
   290  	if err != nil {
   291  		return err
   292  	}
   293  	err = cmdutil.EnsureMongoServer(ensureServerParams)
   294  	if err != nil {
   295  		return err
   296  	}
   297  
   298  	peerAddr := mongo.SelectPeerAddress(addrs)
   299  	if peerAddr == "" {
   300  		return fmt.Errorf("no appropriate peer address found in %q", addrs)
   301  	}
   302  	peerHostPort := net.JoinHostPort(peerAddr, fmt.Sprint(servingInfo.StatePort))
   303  
   304  	return maybeInitiateMongoServer(peergrouper.InitiateMongoParams{
   305  		DialInfo:       dialInfo,
   306  		MemberHostPort: peerHostPort,
   307  	})
   308  }
   309  
   310  // populateDefaultStoragePools creates the default storage pools.
   311  func (c *BootstrapCommand) populateDefaultStoragePools(st *state.State) error {
   312  	settings := state.NewStateSettings(st)
   313  	return poolmanager.AddDefaultStoragePools(settings)
   314  }
   315  
   316  // populateTools stores uploaded tools in provider storage
   317  // and updates the tools metadata.
   318  func (c *BootstrapCommand) populateTools(st *state.State, env environs.Environ) error {
   319  	agentConfig := c.CurrentConfig()
   320  	dataDir := agentConfig.DataDir()
   321  	tools, err := agenttools.ReadTools(dataDir, version.Current)
   322  	if err != nil {
   323  		return errors.Trace(err)
   324  	}
   325  
   326  	data, err := ioutil.ReadFile(filepath.Join(
   327  		agenttools.SharedToolsDir(dataDir, version.Current),
   328  		"tools.tar.gz",
   329  	))
   330  	if err != nil {
   331  		return errors.Trace(err)
   332  	}
   333  
   334  	storage, err := st.ToolsStorage()
   335  	if err != nil {
   336  		return errors.Trace(err)
   337  	}
   338  	defer storage.Close()
   339  
   340  	var toolsVersions []version.Binary
   341  	if strings.HasPrefix(tools.URL, "file://") {
   342  		// Tools were uploaded: clone for each series of the same OS.
   343  		os, err := series.GetOSFromSeries(tools.Version.Series)
   344  		if err != nil {
   345  			return errors.Trace(err)
   346  		}
   347  		osSeries := series.OSSupportedSeries(os)
   348  		for _, series := range osSeries {
   349  			toolsVersion := tools.Version
   350  			toolsVersion.Series = series
   351  			toolsVersions = append(toolsVersions, toolsVersion)
   352  		}
   353  	} else {
   354  		// Tools were downloaded from an external source: don't clone.
   355  		toolsVersions = []version.Binary{tools.Version}
   356  	}
   357  
   358  	for _, toolsVersion := range toolsVersions {
   359  		metadata := toolstorage.Metadata{
   360  			Version: toolsVersion,
   361  			Size:    tools.Size,
   362  			SHA256:  tools.SHA256,
   363  		}
   364  		logger.Debugf("Adding tools: %v", toolsVersion)
   365  		if err := storage.AddTools(bytes.NewReader(data), metadata); err != nil {
   366  			return errors.Trace(err)
   367  		}
   368  	}
   369  	return nil
   370  }
   371  
   372  // storeCustomImageMetadata reads the custom image metadata from disk,
   373  // and stores the files in environment storage with the same relative
   374  // paths.
   375  func (c *BootstrapCommand) storeCustomImageMetadata(stor storage.Storage) error {
   376  	logger.Debugf("storing custom image metadata from %q", c.ImageMetadataDir)
   377  	return filepath.Walk(c.ImageMetadataDir, func(abspath string, info os.FileInfo, err error) error {
   378  		if err != nil {
   379  			return err
   380  		}
   381  		if !info.Mode().IsRegular() {
   382  			return nil
   383  		}
   384  		relpath, err := filepath.Rel(c.ImageMetadataDir, abspath)
   385  		if err != nil {
   386  			return err
   387  		}
   388  		f, err := os.Open(abspath)
   389  		if err != nil {
   390  			return err
   391  		}
   392  		defer f.Close()
   393  		relpath = filepath.ToSlash(relpath)
   394  		logger.Debugf("storing %q in environment storage (%d bytes)", relpath, info.Size())
   395  		return stor.Put(relpath, f, info.Size())
   396  	})
   397  }
   398  
   399  // Override for testing.
   400  var seriesFromVersion = series.VersionSeries
   401  
   402  // saveCustomImageMetadata reads the custom image metadata from disk,
   403  // and saves it in state server.
   404  func (c *BootstrapCommand) saveCustomImageMetadata(st *state.State) error {
   405  	logger.Debugf("saving custom image metadata from %q", c.ImageMetadataDir)
   406  
   407  	baseURL := fmt.Sprintf("file://%s", filepath.ToSlash(c.ImageMetadataDir))
   408  	datasource := simplestreams.NewURLDataSource("bootstrap metadata", baseURL, utils.NoVerifySSLHostnames)
   409  
   410  	// Read the image metadata, as we'll want to upload it to the environment.
   411  	imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{})
   412  	existingMetadata, _, err := imagemetadata.Fetch(
   413  		[]simplestreams.DataSource{datasource}, imageConstraint, false)
   414  	if err != nil && !errors.IsNotFound(err) {
   415  		return errors.Annotate(err, "cannot read image metadata")
   416  	}
   417  
   418  	if len(existingMetadata) == 0 {
   419  		return nil
   420  	}
   421  	msg := ""
   422  	for _, one := range existingMetadata {
   423  		m := cloudimagemetadata.Metadata{
   424  			cloudimagemetadata.MetadataAttributes{
   425  				Stream:          one.Stream,
   426  				Region:          one.RegionName,
   427  				Arch:            one.Arch,
   428  				VirtType:        one.VirtType,
   429  				RootStorageType: one.Storage,
   430  				Source:          "custom",
   431  			},
   432  			one.Id,
   433  		}
   434  		s, err := seriesFromVersion(one.Version)
   435  		if err != nil {
   436  			return errors.Annotatef(err, "cannot determine series for version %v", one.Version)
   437  		}
   438  		m.Series = s
   439  		err = st.CloudImageMetadataStorage.SaveMetadata(m)
   440  		if err != nil {
   441  			return errors.Annotatef(err, "cannot cache image metadata %v", m)
   442  		}
   443  	}
   444  	if len(msg) > 0 {
   445  		return errors.New(msg)
   446  	}
   447  	return nil
   448  }
   449  
   450  // yamlBase64Value implements gnuflag.Value on a map[string]interface{}.
   451  type yamlBase64Value map[string]interface{}
   452  
   453  // Set decodes the base64 value into yaml then expands that into a map.
   454  func (v *yamlBase64Value) Set(value string) error {
   455  	decoded, err := base64.StdEncoding.DecodeString(value)
   456  	if err != nil {
   457  		return err
   458  	}
   459  	return goyaml.Unmarshal(decoded, v)
   460  }
   461  
   462  func (v *yamlBase64Value) String() string {
   463  	return fmt.Sprintf("%v", *v)
   464  }
   465  
   466  // yamlBase64Var sets up a gnuflag flag analogous to the FlagSet.*Var methods.
   467  func yamlBase64Var(fs *gnuflag.FlagSet, target *map[string]interface{}, name string, value string, usage string) {
   468  	fs.Var((*yamlBase64Value)(target), name, usage)
   469  }