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