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