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 }