github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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/arch" 23 "github.com/juju/utils/series" 24 "github.com/juju/utils/ssh" 25 "github.com/juju/version" 26 goyaml "gopkg.in/yaml.v2" 27 "launchpad.net/gnuflag" 28 29 "github.com/juju/juju/agent" 30 "github.com/juju/juju/agent/agentbootstrap" 31 agenttools "github.com/juju/juju/agent/tools" 32 agentcmd "github.com/juju/juju/cmd/jujud/agent" 33 cmdutil "github.com/juju/juju/cmd/jujud/util" 34 "github.com/juju/juju/constraints" 35 "github.com/juju/juju/environs" 36 "github.com/juju/juju/environs/config" 37 "github.com/juju/juju/environs/imagemetadata" 38 "github.com/juju/juju/environs/simplestreams" 39 envtools "github.com/juju/juju/environs/tools" 40 "github.com/juju/juju/instance" 41 "github.com/juju/juju/mongo" 42 "github.com/juju/juju/network" 43 "github.com/juju/juju/state" 44 "github.com/juju/juju/state/binarystorage" 45 "github.com/juju/juju/state/cloudimagemetadata" 46 "github.com/juju/juju/state/multiwatcher" 47 "github.com/juju/juju/state/storage" 48 "github.com/juju/juju/storage/poolmanager" 49 "github.com/juju/juju/tools" 50 jujuversion "github.com/juju/juju/version" 51 "github.com/juju/juju/worker/peergrouper" 52 ) 53 54 var ( 55 initiateMongoServer = peergrouper.InitiateMongoServer 56 agentInitializeState = agentbootstrap.InitializeState 57 sshGenerateKey = ssh.GenerateKey 58 newStateStorage = storage.NewStorage 59 minSocketTimeout = 1 * time.Minute 60 logger = loggo.GetLogger("juju.cmd.jujud") 61 ) 62 63 // BootstrapCommand represents a jujud bootstrap command. 64 type BootstrapCommand struct { 65 cmd.CommandBase 66 agentcmd.AgentConf 67 ControllerModelConfig map[string]interface{} 68 HostedModelConfig map[string]interface{} 69 BootstrapConstraints constraints.Value 70 ModelConstraints constraints.Value 71 Hardware instance.HardwareCharacteristics 72 InstanceId string 73 AdminUsername string 74 ImageMetadataDir string 75 } 76 77 // NewBootstrapCommand returns a new BootstrapCommand that has been initialized. 78 func NewBootstrapCommand() *BootstrapCommand { 79 return &BootstrapCommand{ 80 AgentConf: agentcmd.NewAgentConf(""), 81 } 82 } 83 84 // Info returns a decription of the command. 85 func (c *BootstrapCommand) Info() *cmd.Info { 86 return &cmd.Info{ 87 Name: "bootstrap-state", 88 Purpose: "initialize juju state", 89 } 90 } 91 92 // SetFlags adds the flags for this command to the passed gnuflag.FlagSet. 93 func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) { 94 c.AgentConf.AddFlags(f) 95 yamlBase64Var(f, &c.ControllerModelConfig, "model-config", "", "controller model configuration (yaml, base64 encoded)") 96 yamlBase64Var(f, &c.HostedModelConfig, "hosted-model-config", "", "initial hosted model configuration (yaml, base64 encoded)") 97 f.Var(constraints.ConstraintsValue{Target: &c.BootstrapConstraints}, "bootstrap-constraints", "bootstrap machine constraints (space-separated strings)") 98 f.Var(constraints.ConstraintsValue{Target: &c.ModelConstraints}, "constraints", "initial constraints (space-separated strings)") 99 f.Var(&c.Hardware, "hardware", "hardware characteristics (space-separated strings)") 100 f.StringVar(&c.InstanceId, "instance-id", "", "unique instance-id for bootstrap machine") 101 f.StringVar(&c.AdminUsername, "admin-user", "admin", "set the name for the juju admin user") 102 f.StringVar(&c.ImageMetadataDir, "image-metadata", "", "custom image metadata source dir") 103 } 104 105 // Init initializes the command for running. 106 func (c *BootstrapCommand) Init(args []string) error { 107 if len(c.ControllerModelConfig) == 0 { 108 return cmdutil.RequiredError("model-config") 109 } 110 if len(c.HostedModelConfig) == 0 { 111 return cmdutil.RequiredError("hosted-model-config") 112 } 113 if c.InstanceId == "" { 114 return cmdutil.RequiredError("instance-id") 115 } 116 if !names.IsValidUser(c.AdminUsername) { 117 return errors.Errorf("%q is not a valid username", c.AdminUsername) 118 } 119 return c.AgentConf.CheckArgs(args) 120 } 121 122 // Run initializes state for an environment. 123 func (c *BootstrapCommand) Run(_ *cmd.Context) error { 124 envCfg, err := config.New(config.NoDefaults, c.ControllerModelConfig) 125 if err != nil { 126 return err 127 } 128 err = c.ReadConfig("machine-0") 129 if err != nil { 130 return err 131 } 132 agentConfig := c.CurrentConfig() 133 network.SetPreferIPv6(agentConfig.PreferIPv6()) 134 135 // agent.Jobs is an optional field in the agent config, and was 136 // introduced after 1.17.2. We default to allowing units on 137 // machine-0 if missing. 138 jobs := agentConfig.Jobs() 139 if len(jobs) == 0 { 140 jobs = []multiwatcher.MachineJob{ 141 multiwatcher.JobManageModel, 142 multiwatcher.JobHostUnits, 143 multiwatcher.JobManageNetworking, 144 } 145 } 146 147 // Get the bootstrap machine's addresses from the provider. 148 env, err := environs.New(envCfg) 149 if err != nil { 150 return err 151 } 152 newConfigAttrs := make(map[string]interface{}) 153 154 // Check to see if a newer agent version has been requested 155 // by the bootstrap client. 156 desiredVersion, ok := envCfg.AgentVersion() 157 if ok && desiredVersion != jujuversion.Current { 158 // If we have been asked for a newer version, ensure the newer 159 // tools can actually be found, or else bootstrap won't complete. 160 stream := envtools.PreferredStream(&desiredVersion, envCfg.Development(), envCfg.AgentStream()) 161 logger.Infof("newer tools requested, looking for %v in stream %v", desiredVersion, stream) 162 filter := tools.Filter{ 163 Number: desiredVersion, 164 Arch: arch.HostArch(), 165 Series: series.HostSeries(), 166 } 167 _, toolsErr := envtools.FindTools(env, -1, -1, stream, filter) 168 if toolsErr == nil { 169 logger.Infof("tools are available, upgrade will occur after bootstrap") 170 } 171 if errors.IsNotFound(toolsErr) { 172 // Newer tools not available, so revert to using the tools 173 // matching the current agent version. 174 logger.Warningf("newer tools for %q not available, sticking with version %q", desiredVersion, jujuversion.Current) 175 newConfigAttrs["agent-version"] = jujuversion.Current.String() 176 } else if toolsErr != nil { 177 logger.Errorf("cannot find newer tools: %v", toolsErr) 178 return toolsErr 179 } 180 } 181 182 instanceId := instance.Id(c.InstanceId) 183 instances, err := env.Instances([]instance.Id{instanceId}) 184 if err != nil { 185 return err 186 } 187 addrs, err := instances[0].Addresses() 188 if err != nil { 189 return err 190 } 191 192 // When machine addresses are reported from state, they have 193 // duplicates removed. We should do the same here so that 194 // there is not unnecessary churn in the mongo replicaset. 195 // TODO (cherylj) Add explicit unit tests for this - tracked 196 // by bug #1544158. 197 addrs = network.MergedAddresses([]network.Address{}, addrs) 198 199 // Generate a private SSH key for the controllers, and add 200 // the public key to the environment config. We'll add the 201 // private key to StateServingInfo below. 202 privateKey, publicKey, err := sshGenerateKey(config.JujuSystemKey) 203 if err != nil { 204 return errors.Annotate(err, "failed to generate system key") 205 } 206 authorizedKeys := config.ConcatAuthKeys(envCfg.AuthorizedKeys(), publicKey) 207 newConfigAttrs[config.AuthKeysConfig] = authorizedKeys 208 209 // Generate a shared secret for the Mongo replica set, and write it out. 210 sharedSecret, err := mongo.GenerateSharedSecret() 211 if err != nil { 212 return err 213 } 214 info, ok := agentConfig.StateServingInfo() 215 if !ok { 216 return fmt.Errorf("bootstrap machine config has no state serving info") 217 } 218 info.SharedSecret = sharedSecret 219 info.SystemIdentity = privateKey 220 err = c.ChangeConfig(func(agentConfig agent.ConfigSetter) error { 221 agentConfig.SetStateServingInfo(info) 222 return nil 223 }) 224 if err != nil { 225 return fmt.Errorf("cannot write agent config: %v", err) 226 } 227 228 agentConfig = c.CurrentConfig() 229 230 // Create system-identity file 231 if err := agent.WriteSystemIdentityFile(agentConfig); err != nil { 232 return err 233 } 234 235 if err := c.startMongo(addrs, agentConfig); err != nil { 236 return err 237 } 238 239 logger.Infof("started mongo") 240 // Initialise state, and store any agent config (e.g. password) changes. 241 envCfg, err = env.Config().Apply(newConfigAttrs) 242 if err != nil { 243 return errors.Annotate(err, "failed to update model config") 244 } 245 var st *state.State 246 var m *state.Machine 247 err = c.ChangeConfig(func(agentConfig agent.ConfigSetter) error { 248 var stateErr error 249 dialOpts := mongo.DefaultDialOpts() 250 251 // Set a longer socket timeout than usual, as the machine 252 // will be starting up and disk I/O slower than usual. This 253 // has been known to cause timeouts in queries. 254 timeouts := envCfg.BootstrapSSHOpts() 255 dialOpts.SocketTimeout = timeouts.Timeout 256 if dialOpts.SocketTimeout < minSocketTimeout { 257 dialOpts.SocketTimeout = minSocketTimeout 258 } 259 260 // We shouldn't attempt to dial peers until we have some. 261 dialOpts.Direct = true 262 263 adminTag := names.NewLocalUserTag(c.AdminUsername) 264 st, m, stateErr = agentInitializeState( 265 adminTag, 266 agentConfig, 267 envCfg, c.HostedModelConfig, 268 agentbootstrap.BootstrapMachineConfig{ 269 Addresses: addrs, 270 BootstrapConstraints: c.BootstrapConstraints, 271 ModelConstraints: c.ModelConstraints, 272 Jobs: jobs, 273 InstanceId: instanceId, 274 Characteristics: c.Hardware, 275 SharedSecret: sharedSecret, 276 }, 277 dialOpts, 278 environs.NewStatePolicy(), 279 ) 280 return stateErr 281 }) 282 if err != nil { 283 return err 284 } 285 defer st.Close() 286 287 // Populate the tools catalogue. 288 if err := c.populateTools(st, env); err != nil { 289 return err 290 } 291 292 // Populate the GUI archive catalogue. 293 if err := c.populateGUIArchive(st, env); err != nil { 294 // Do not stop the bootstrapping process for Juju GUI archive errors. 295 logger.Warningf("cannot set up Juju GUI: %s", err) 296 } else { 297 logger.Debugf("Juju GUI successfully set up") 298 } 299 300 // Add custom image metadata to environment storage. 301 if c.ImageMetadataDir != "" { 302 if err := c.saveCustomImageMetadata(st, env); err != nil { 303 return err 304 } 305 306 stor := newStateStorage(st.ModelUUID(), st.MongoSession()) 307 if err := c.storeCustomImageMetadata(stor); err != nil { 308 return err 309 } 310 } 311 312 // Populate the storage pools. 313 if err = c.populateDefaultStoragePools(st); err != nil { 314 return err 315 } 316 317 // bootstrap machine always gets the vote 318 return m.SetHasVote(true) 319 } 320 321 func (c *BootstrapCommand) startMongo(addrs []network.Address, agentConfig agent.Config) error { 322 logger.Debugf("starting mongo") 323 324 info, ok := agentConfig.MongoInfo() 325 if !ok { 326 return fmt.Errorf("no state info available") 327 } 328 // When bootstrapping, we need to allow enough time for mongo 329 // to start as there's no retry loop in place. 330 // 5 minutes should suffice. 331 bootstrapDialOpts := mongo.DialOpts{Timeout: 5 * time.Minute} 332 dialInfo, err := mongo.DialInfo(info.Info, bootstrapDialOpts) 333 if err != nil { 334 return err 335 } 336 servingInfo, ok := agentConfig.StateServingInfo() 337 if !ok { 338 return fmt.Errorf("agent config has no state serving info") 339 } 340 // Use localhost to dial the mongo server, because it's running in 341 // auth mode and will refuse to perform any operations unless 342 // we dial that address. 343 dialInfo.Addrs = []string{ 344 net.JoinHostPort("127.0.0.1", fmt.Sprint(servingInfo.StatePort)), 345 } 346 347 logger.Debugf("calling ensureMongoServer") 348 ensureServerParams, err := cmdutil.NewEnsureServerParams(agentConfig) 349 if err != nil { 350 return err 351 } 352 err = cmdutil.EnsureMongoServer(ensureServerParams) 353 if err != nil { 354 return err 355 } 356 357 peerAddr := mongo.SelectPeerAddress(addrs) 358 if peerAddr == "" { 359 return fmt.Errorf("no appropriate peer address found in %q", addrs) 360 } 361 peerHostPort := net.JoinHostPort(peerAddr, fmt.Sprint(servingInfo.StatePort)) 362 363 return initiateMongoServer(peergrouper.InitiateMongoParams{ 364 DialInfo: dialInfo, 365 MemberHostPort: peerHostPort, 366 }) 367 } 368 369 // populateDefaultStoragePools creates the default storage pools. 370 func (c *BootstrapCommand) populateDefaultStoragePools(st *state.State) error { 371 settings := state.NewStateSettings(st) 372 return poolmanager.AddDefaultStoragePools(settings) 373 } 374 375 // populateTools stores uploaded tools in provider storage 376 // and updates the tools metadata. 377 func (c *BootstrapCommand) populateTools(st *state.State, env environs.Environ) error { 378 agentConfig := c.CurrentConfig() 379 dataDir := agentConfig.DataDir() 380 381 current := version.Binary{ 382 Number: jujuversion.Current, 383 Arch: arch.HostArch(), 384 Series: series.HostSeries(), 385 } 386 tools, err := agenttools.ReadTools(dataDir, current) 387 if err != nil { 388 return errors.Trace(err) 389 } 390 391 data, err := ioutil.ReadFile(filepath.Join( 392 agenttools.SharedToolsDir(dataDir, current), 393 "tools.tar.gz", 394 )) 395 if err != nil { 396 return errors.Trace(err) 397 } 398 399 toolstorage, err := st.ToolsStorage() 400 if err != nil { 401 return errors.Trace(err) 402 } 403 defer toolstorage.Close() 404 405 var toolsVersions []version.Binary 406 if strings.HasPrefix(tools.URL, "file://") { 407 // Tools were uploaded: clone for each series of the same OS. 408 os, err := series.GetOSFromSeries(tools.Version.Series) 409 if err != nil { 410 return errors.Trace(err) 411 } 412 osSeries := series.OSSupportedSeries(os) 413 for _, series := range osSeries { 414 toolsVersion := tools.Version 415 toolsVersion.Series = series 416 toolsVersions = append(toolsVersions, toolsVersion) 417 } 418 } else { 419 // Tools were downloaded from an external source: don't clone. 420 toolsVersions = []version.Binary{tools.Version} 421 } 422 423 for _, toolsVersion := range toolsVersions { 424 metadata := binarystorage.Metadata{ 425 Version: toolsVersion.String(), 426 Size: tools.Size, 427 SHA256: tools.SHA256, 428 } 429 logger.Debugf("Adding tools: %v", toolsVersion) 430 if err := toolstorage.Add(bytes.NewReader(data), metadata); err != nil { 431 return errors.Trace(err) 432 } 433 } 434 return nil 435 } 436 437 // populateGUIArchive stores the uploaded Juju GUI archive in provider storage, 438 // updates the GUI metadata and set the current Juju GUI version. 439 func (c *BootstrapCommand) populateGUIArchive(st *state.State, env environs.Environ) error { 440 agentConfig := c.CurrentConfig() 441 dataDir := agentConfig.DataDir() 442 guistorage, err := st.GUIStorage() 443 if err != nil { 444 return errors.Trace(err) 445 } 446 defer guistorage.Close() 447 gui, err := agenttools.ReadGUIArchive(dataDir) 448 if err != nil { 449 return errors.Annotate(err, "cannot fetch GUI info") 450 } 451 f, err := os.Open(filepath.Join(agenttools.SharedGUIDir(dataDir), "gui.tar.bz2")) 452 if err != nil { 453 return errors.Annotate(err, "cannot read GUI archive") 454 } 455 defer f.Close() 456 if err := guistorage.Add(f, binarystorage.Metadata{ 457 Version: gui.Version.String(), 458 Size: gui.Size, 459 SHA256: gui.SHA256, 460 }); err != nil { 461 return errors.Annotate(err, "cannot store GUI archive") 462 } 463 if err = st.GUISetVersion(gui.Version); err != nil { 464 return errors.Annotate(err, "cannot set current GUI version") 465 } 466 return nil 467 } 468 469 // storeCustomImageMetadata reads the custom image metadata from disk, 470 // and stores the files in environment storage with the same relative 471 // paths. 472 func (c *BootstrapCommand) storeCustomImageMetadata(stor storage.Storage) error { 473 logger.Debugf("storing custom image metadata from %q", c.ImageMetadataDir) 474 return filepath.Walk(c.ImageMetadataDir, func(abspath string, info os.FileInfo, err error) error { 475 if err != nil { 476 return err 477 } 478 if !info.Mode().IsRegular() { 479 return nil 480 } 481 relpath, err := filepath.Rel(c.ImageMetadataDir, abspath) 482 if err != nil { 483 return err 484 } 485 f, err := os.Open(abspath) 486 if err != nil { 487 return err 488 } 489 defer f.Close() 490 relpath = filepath.ToSlash(relpath) 491 logger.Debugf("storing %q in model storage (%d bytes)", relpath, info.Size()) 492 return stor.Put(relpath, f, info.Size()) 493 }) 494 } 495 496 // Override for testing. 497 var seriesFromVersion = series.VersionSeries 498 499 // saveCustomImageMetadata reads the custom image metadata from disk, 500 // and saves it in controller. 501 func (c *BootstrapCommand) saveCustomImageMetadata(st *state.State, env environs.Environ) error { 502 logger.Debugf("saving custom image metadata from %q", c.ImageMetadataDir) 503 baseURL := fmt.Sprintf("file://%s", filepath.ToSlash(c.ImageMetadataDir)) 504 publicKey, _ := simplestreams.UserPublicSigningKey() 505 datasource := simplestreams.NewURLSignedDataSource("custom", baseURL, publicKey, utils.NoVerifySSLHostnames, simplestreams.CUSTOM_CLOUD_DATA, false) 506 return storeImageMetadataFromFiles(st, env, datasource) 507 } 508 509 // storeImageMetadataFromFiles puts image metadata found in sources into state. 510 // Declared as var to facilitate tests. 511 var storeImageMetadataFromFiles = func(st *state.State, env environs.Environ, source simplestreams.DataSource) error { 512 // Read the image metadata, as we'll want to upload it to the environment. 513 imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{}) 514 if inst, ok := env.(simplestreams.HasRegion); ok { 515 // If we can determine current region, 516 // we want only metadata specific to this region. 517 cloud, err := inst.Region() 518 if err != nil { 519 return err 520 } 521 imageConstraint.CloudSpec = cloud 522 } 523 524 existingMetadata, info, err := imagemetadata.Fetch([]simplestreams.DataSource{source}, imageConstraint) 525 if err != nil && !errors.IsNotFound(err) { 526 return errors.Annotate(err, "cannot read image metadata") 527 } 528 return storeImageMetadataInState(st, info.Source, source.Priority(), existingMetadata) 529 } 530 531 // storeImageMetadataInState writes image metadata into state store. 532 func storeImageMetadataInState(st *state.State, source string, priority int, existingMetadata []*imagemetadata.ImageMetadata) error { 533 if len(existingMetadata) == 0 { 534 return nil 535 } 536 metadataState := make([]cloudimagemetadata.Metadata, len(existingMetadata)) 537 for i, one := range existingMetadata { 538 m := cloudimagemetadata.Metadata{ 539 cloudimagemetadata.MetadataAttributes{ 540 Stream: one.Stream, 541 Region: one.RegionName, 542 Arch: one.Arch, 543 VirtType: one.VirtType, 544 RootStorageType: one.Storage, 545 Source: source, 546 }, 547 priority, 548 one.Id, 549 } 550 s, err := seriesFromVersion(one.Version) 551 if err != nil { 552 return errors.Annotatef(err, "cannot determine series for version %v", one.Version) 553 } 554 m.Series = s 555 metadataState[i] = m 556 } 557 if err := st.CloudImageMetadataStorage.SaveMetadata(metadataState); err != nil { 558 return errors.Annotatef(err, "cannot cache image metadata") 559 } 560 return nil 561 } 562 563 // yamlBase64Value implements gnuflag.Value on a map[string]interface{}. 564 type yamlBase64Value map[string]interface{} 565 566 // Set decodes the base64 value into yaml then expands that into a map. 567 func (v *yamlBase64Value) Set(value string) error { 568 decoded, err := base64.StdEncoding.DecodeString(value) 569 if err != nil { 570 return err 571 } 572 return goyaml.Unmarshal(decoded, v) 573 } 574 575 func (v *yamlBase64Value) String() string { 576 return fmt.Sprintf("%v", *v) 577 } 578 579 // yamlBase64Var sets up a gnuflag flag analogous to the FlagSet.*Var methods. 580 func yamlBase64Var(fs *gnuflag.FlagSet, target *map[string]interface{}, name string, value string, usage string) { 581 fs.Var((*yamlBase64Value)(target), name, usage) 582 }