github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 "fmt" 9 "io/ioutil" 10 "net" 11 "os" 12 "path/filepath" 13 "strings" 14 "time" 15 16 "github.com/juju/cmd" 17 "github.com/juju/errors" 18 "github.com/juju/gnuflag" 19 "github.com/juju/loggo" 20 "github.com/juju/os/series" 21 "github.com/juju/utils/arch" 22 "github.com/juju/utils/ssh" 23 "github.com/juju/version" 24 "gopkg.in/juju/names.v2" 25 26 "github.com/juju/juju/agent" 27 "github.com/juju/juju/agent/agentbootstrap" 28 agenttools "github.com/juju/juju/agent/tools" 29 "github.com/juju/juju/cloudconfig/instancecfg" 30 jujucmd "github.com/juju/juju/cmd" 31 agentcmd "github.com/juju/juju/cmd/jujud/agent" 32 cmdutil "github.com/juju/juju/cmd/jujud/util" 33 "github.com/juju/juju/core/instance" 34 "github.com/juju/juju/environs" 35 "github.com/juju/juju/environs/config" 36 "github.com/juju/juju/environs/context" 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/mongo" 41 "github.com/juju/juju/network" 42 "github.com/juju/juju/state" 43 "github.com/juju/juju/state/binarystorage" 44 "github.com/juju/juju/state/cloudimagemetadata" 45 "github.com/juju/juju/state/multiwatcher" 46 "github.com/juju/juju/state/stateenvirons" 47 "github.com/juju/juju/tools" 48 jujuversion "github.com/juju/juju/version" 49 "github.com/juju/juju/worker/peergrouper" 50 ) 51 52 var ( 53 initiateMongoServer = peergrouper.InitiateMongoServer 54 agentInitializeState = agentbootstrap.InitializeState 55 sshGenerateKey = ssh.GenerateKey 56 minSocketTimeout = 1 * time.Minute 57 logger = loggo.GetLogger("juju.cmd.jujud") 58 ) 59 60 const adminUserName = "admin" 61 62 // BootstrapCommand represents a jujud bootstrap command. 63 type BootstrapCommand struct { 64 cmd.CommandBase 65 agentcmd.AgentConf 66 BootstrapParamsFile string 67 Timeout time.Duration 68 } 69 70 // NewBootstrapCommand returns a new BootstrapCommand that has been initialized. 71 func NewBootstrapCommand() *BootstrapCommand { 72 return &BootstrapCommand{ 73 AgentConf: agentcmd.NewAgentConf(""), 74 } 75 } 76 77 // Info returns a description of the command. 78 func (c *BootstrapCommand) Info() *cmd.Info { 79 return jujucmd.Info(&cmd.Info{ 80 Name: "bootstrap-state", 81 Purpose: "initialize juju state", 82 }) 83 } 84 85 // SetFlags adds the flags for this command to the passed gnuflag.FlagSet. 86 func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) { 87 c.AgentConf.AddFlags(f) 88 f.DurationVar(&c.Timeout, "timeout", time.Duration(0), "set the bootstrap timeout") 89 } 90 91 // Init initializes the command for running. 92 func (c *BootstrapCommand) Init(args []string) error { 93 if len(args) == 0 { 94 return errors.New("bootstrap-params file must be specified") 95 } 96 if err := cmd.CheckEmpty(args[1:]); err != nil { 97 return err 98 } 99 c.BootstrapParamsFile = args[0] 100 return c.AgentConf.CheckArgs(args[1:]) 101 } 102 103 // EnvironsNew defines function used to get reference to an underlying cloud provider. 104 var EnvironsNew = environs.New 105 106 // Run initializes state for an environment. 107 func (c *BootstrapCommand) Run(_ *cmd.Context) error { 108 bootstrapParamsData, err := ioutil.ReadFile(c.BootstrapParamsFile) 109 if err != nil { 110 return errors.Annotate(err, "reading bootstrap params file") 111 } 112 var args instancecfg.StateInitializationParams 113 if err := args.Unmarshal(bootstrapParamsData); err != nil { 114 return errors.Trace(err) 115 } 116 117 err = c.ReadConfig("machine-0") 118 if err != nil { 119 return errors.Annotate(err, "cannot read config") 120 } 121 agentConfig := c.CurrentConfig() 122 123 // agent.Jobs is an optional field in the agent config, and was 124 // introduced after 1.17.2. We default to allowing units on 125 // machine-0 if missing. 126 jobs := agentConfig.Jobs() 127 if len(jobs) == 0 { 128 jobs = []multiwatcher.MachineJob{ 129 multiwatcher.JobManageModel, 130 multiwatcher.JobHostUnits, 131 } 132 } 133 134 // Get the bootstrap machine's addresses from the provider. 135 cloudSpec, err := environs.MakeCloudSpec( 136 args.ControllerCloud, 137 args.ControllerCloudRegion, 138 args.ControllerCloudCredential, 139 ) 140 if err != nil { 141 return errors.Trace(err) 142 } 143 env, err := EnvironsNew(environs.OpenParams{ 144 Cloud: cloudSpec, 145 Config: args.ControllerModelConfig, 146 }) 147 if err != nil { 148 return errors.Annotate(err, "new environ") 149 } 150 newConfigAttrs := make(map[string]interface{}) 151 152 // Check to see if a newer agent version has been requested 153 // by the bootstrap client. 154 desiredVersion, ok := args.ControllerModelConfig.AgentVersion() 155 if ok && desiredVersion != jujuversion.Current { 156 // If we have been asked for a newer version, ensure the newer 157 // tools can actually be found, or else bootstrap won't complete. 158 streams := envtools.PreferredStreams(&desiredVersion, args.ControllerModelConfig.Development(), args.ControllerModelConfig.AgentStream()) 159 logger.Infof("newer agent binaries requested, looking for %v in streams: %v", desiredVersion, strings.Join(streams, ",")) 160 hostSeries, err := series.HostSeries() 161 if err != nil { 162 return errors.Trace(err) 163 } 164 filter := tools.Filter{ 165 Number: desiredVersion, 166 Arch: arch.HostArch(), 167 Series: hostSeries, 168 } 169 _, toolsErr := envtools.FindTools(env, -1, -1, streams, filter) 170 if toolsErr == nil { 171 logger.Infof("agent binaries are available, upgrade will occur after bootstrap") 172 } 173 if errors.IsNotFound(toolsErr) { 174 // Newer tools not available, so revert to using the tools 175 // matching the current agent version. 176 logger.Warningf("newer agent binaries for %q not available, sticking with version %q", desiredVersion, jujuversion.Current) 177 newConfigAttrs["agent-version"] = jujuversion.Current.String() 178 } else if toolsErr != nil { 179 logger.Errorf("cannot find newer agent binaries: %v", toolsErr) 180 return toolsErr 181 } 182 } 183 184 callCtx := context.NewCloudCallContext() 185 // At this stage, cloud credential has not yet been stored server-side 186 // as there is no server-side. If these cloud calls will fail with 187 // invalid credential, just log it. 188 callCtx.InvalidateCredentialFunc = func(reason string) error { 189 logger.Errorf("Cloud credential %q is not accepted by cloud provider: %v", args.ControllerCloudCredentialName, reason) 190 return nil 191 } 192 193 instances, err := env.Instances(callCtx, []instance.Id{args.BootstrapMachineInstanceId}) 194 if err != nil { 195 return errors.Annotate(err, "getting bootstrap instance") 196 } 197 addrs, err := instances[0].Addresses(callCtx) 198 if err != nil { 199 return errors.Annotate(err, "bootstrap instance addresses") 200 } 201 202 // When machine addresses are reported from state, they have 203 // duplicates removed. We should do the same here so that 204 // there is not unnecessary churn in the mongo replicaset. 205 // TODO (cherylj) Add explicit unit tests for this - tracked 206 // by bug #1544158. 207 addrs = network.MergedAddresses([]network.Address{}, addrs) 208 209 // Generate a private SSH key for the controllers, and add 210 // the public key to the environment config. We'll add the 211 // private key to StateServingInfo below. 212 privateKey, publicKey, err := sshGenerateKey(config.JujuSystemKey) 213 if err != nil { 214 return errors.Annotate(err, "failed to generate system key") 215 } 216 authorizedKeys := config.ConcatAuthKeys(args.ControllerModelConfig.AuthorizedKeys(), publicKey) 217 newConfigAttrs[config.AuthorizedKeysKey] = authorizedKeys 218 219 // Generate a shared secret for the Mongo replica set, and write it out. 220 sharedSecret, err := mongo.GenerateSharedSecret() 221 if err != nil { 222 return err 223 } 224 info, ok := agentConfig.StateServingInfo() 225 if !ok { 226 return fmt.Errorf("bootstrap machine config has no state serving info") 227 } 228 info.SharedSecret = sharedSecret 229 info.SystemIdentity = privateKey 230 err = c.ChangeConfig(func(agentConfig agent.ConfigSetter) error { 231 agentConfig.SetStateServingInfo(info) 232 mmprof, err := mongo.NewMemoryProfile(args.ControllerConfig.MongoMemoryProfile()) 233 if err != nil { 234 logger.Errorf("could not set requested memory profile: %v", err) 235 } else { 236 agentConfig.SetMongoMemoryProfile(mmprof) 237 } 238 return nil 239 }) 240 if err != nil { 241 return fmt.Errorf("cannot write agent config: %v", err) 242 } 243 244 agentConfig = c.CurrentConfig() 245 246 // Create system-identity file 247 if err := agent.WriteSystemIdentityFile(agentConfig); err != nil { 248 return err 249 } 250 251 if err := c.startMongo(addrs, agentConfig); err != nil { 252 return errors.Annotate(err, "failed to start mongo") 253 } 254 255 controllerModelCfg, err := env.Config().Apply(newConfigAttrs) 256 if err != nil { 257 return errors.Annotate(err, "failed to update model config") 258 } 259 args.ControllerModelConfig = controllerModelCfg 260 261 // Initialise state, and store any agent config (e.g. password) changes. 262 var controller *state.Controller 263 var m *state.Machine 264 err = c.ChangeConfig(func(agentConfig agent.ConfigSetter) error { 265 var stateErr error 266 dialOpts := mongo.DefaultDialOpts() 267 268 // Set a longer socket timeout than usual, as the machine 269 // will be starting up and disk I/O slower than usual. This 270 // has been known to cause timeouts in queries. 271 dialOpts.SocketTimeout = c.Timeout 272 if dialOpts.SocketTimeout < minSocketTimeout { 273 dialOpts.SocketTimeout = minSocketTimeout 274 } 275 276 // We shouldn't attempt to dial peers until we have some. 277 dialOpts.Direct = true 278 279 adminTag := names.NewLocalUserTag(adminUserName) 280 controller, m, stateErr = agentInitializeState( 281 adminTag, 282 agentConfig, 283 agentbootstrap.InitializeStateParams{ 284 StateInitializationParams: args, 285 BootstrapMachineAddresses: addrs, 286 BootstrapMachineJobs: jobs, 287 SharedSecret: sharedSecret, 288 Provider: environs.Provider, 289 StorageProviderRegistry: stateenvirons.NewStorageProviderRegistry(env), 290 }, 291 dialOpts, 292 stateenvirons.GetNewPolicyFunc(), 293 ) 294 return stateErr 295 }) 296 if err != nil { 297 return err 298 } 299 defer controller.Close() 300 st := controller.SystemState() 301 302 // Set up default container networking mode 303 model, err := st.Model() 304 if err != nil { 305 return err 306 } 307 if err = model.AutoConfigureContainerNetworking(env); err != nil { 308 if errors.IsNotSupported(err) { 309 logger.Debugf("Not performing container networking autoconfiguration on a non-networking environment") 310 } else { 311 return err 312 } 313 } 314 315 // Fetch spaces from substrate 316 if err = st.ReloadSpaces(env); err != nil { 317 if errors.IsNotSupported(err) { 318 logger.Debugf("Not performing spaces load on a non-networking environment") 319 } else { 320 return err 321 } 322 } 323 324 // Populate the tools catalogue. 325 if err := c.populateTools(st, env); err != nil { 326 return err 327 } 328 329 // Populate the GUI archive catalogue. 330 if err := c.populateGUIArchive(st, env); err != nil { 331 // Do not stop the bootstrapping process for Juju GUI archive errors. 332 logger.Warningf("cannot set up Juju GUI: %s", err) 333 } else { 334 logger.Debugf("Juju GUI successfully set up") 335 } 336 337 // Add custom image metadata to environment storage. 338 if len(args.CustomImageMetadata) > 0 { 339 if err := c.saveCustomImageMetadata(st, env, args.CustomImageMetadata); err != nil { 340 return err 341 } 342 } 343 344 // bootstrap machine always gets the vote 345 return m.SetHasVote(true) 346 } 347 348 func (c *BootstrapCommand) startMongo(addrs []network.Address, agentConfig agent.Config) error { 349 logger.Debugf("starting mongo") 350 351 info, ok := agentConfig.MongoInfo() 352 if !ok { 353 return fmt.Errorf("no state info available") 354 } 355 // When bootstrapping, we need to allow enough time for mongo 356 // to start as there's no retry loop in place. 357 // 5 minutes should suffice. 358 mongoDialOpts := mongo.DialOpts{Timeout: 5 * time.Minute} 359 dialInfo, err := mongo.DialInfo(info.Info, mongoDialOpts) 360 if err != nil { 361 return err 362 } 363 servingInfo, ok := agentConfig.StateServingInfo() 364 if !ok { 365 return fmt.Errorf("agent config has no state serving info") 366 } 367 // Use localhost to dial the mongo server, because it's running in 368 // auth mode and will refuse to perform any operations unless 369 // we dial that address. 370 // TODO(macgreagoir) IPv6. Ubuntu still always provides IPv4 loopback, 371 // and when/if this changes localhost should resolve to IPv6 loopback 372 // in any case (lp:1644009). Review. 373 dialInfo.Addrs = []string{ 374 net.JoinHostPort("localhost", fmt.Sprint(servingInfo.StatePort)), 375 } 376 377 logger.Debugf("calling ensureMongoServer") 378 ensureServerParams, err := cmdutil.NewEnsureServerParams(agentConfig) 379 if err != nil { 380 return err 381 } 382 _, err = cmdutil.EnsureMongoServer(ensureServerParams) 383 if err != nil { 384 return err 385 } 386 387 peerAddr := mongo.SelectPeerAddress(addrs) 388 if peerAddr == "" { 389 return fmt.Errorf("no appropriate peer address found in %q", addrs) 390 } 391 peerHostPort := net.JoinHostPort(peerAddr, fmt.Sprint(servingInfo.StatePort)) 392 393 if err := initiateMongoServer(peergrouper.InitiateMongoParams{ 394 DialInfo: dialInfo, 395 MemberHostPort: peerHostPort, 396 }); err != nil { 397 return err 398 } 399 logger.Infof("started mongo") 400 return nil 401 } 402 403 // populateTools stores uploaded tools in provider storage 404 // and updates the tools metadata. 405 func (c *BootstrapCommand) populateTools(st *state.State, env environs.Environ) error { 406 agentConfig := c.CurrentConfig() 407 dataDir := agentConfig.DataDir() 408 409 hostSeries, err := series.HostSeries() 410 if err != nil { 411 return errors.Trace(err) 412 } 413 current := version.Binary{ 414 Number: jujuversion.Current, 415 Arch: arch.HostArch(), 416 Series: hostSeries, 417 } 418 tools, err := agenttools.ReadTools(dataDir, current) 419 if err != nil { 420 return errors.Trace(err) 421 } 422 423 data, err := ioutil.ReadFile(filepath.Join( 424 agenttools.SharedToolsDir(dataDir, current), 425 "tools.tar.gz", 426 )) 427 if err != nil { 428 return errors.Trace(err) 429 } 430 431 toolstorage, err := st.ToolsStorage() 432 if err != nil { 433 return errors.Trace(err) 434 } 435 defer toolstorage.Close() 436 437 var toolsVersions []version.Binary 438 if strings.HasPrefix(tools.URL, "file://") { 439 // Tools were uploaded: clone for each series of the same OS. 440 os, err := series.GetOSFromSeries(tools.Version.Series) 441 if err != nil { 442 return errors.Trace(err) 443 } 444 osSeries := series.OSSupportedSeries(os) 445 for _, series := range osSeries { 446 toolsVersion := tools.Version 447 toolsVersion.Series = series 448 toolsVersions = append(toolsVersions, toolsVersion) 449 } 450 } else { 451 // Tools were downloaded from an external source: don't clone. 452 toolsVersions = []version.Binary{tools.Version} 453 } 454 455 for _, toolsVersion := range toolsVersions { 456 metadata := binarystorage.Metadata{ 457 Version: toolsVersion.String(), 458 Size: tools.Size, 459 SHA256: tools.SHA256, 460 } 461 logger.Debugf("Adding agent binaries: %v", toolsVersion) 462 if err := toolstorage.Add(bytes.NewReader(data), metadata); err != nil { 463 return errors.Trace(err) 464 } 465 } 466 return nil 467 } 468 469 // populateGUIArchive stores the uploaded Juju GUI archive in provider storage, 470 // updates the GUI metadata and set the current Juju GUI version. 471 func (c *BootstrapCommand) populateGUIArchive(st *state.State, env environs.Environ) error { 472 agentConfig := c.CurrentConfig() 473 dataDir := agentConfig.DataDir() 474 guistorage, err := st.GUIStorage() 475 if err != nil { 476 return errors.Trace(err) 477 } 478 defer guistorage.Close() 479 gui, err := agenttools.ReadGUIArchive(dataDir) 480 if err != nil { 481 return errors.Annotate(err, "cannot fetch GUI info") 482 } 483 f, err := os.Open(filepath.Join(agenttools.SharedGUIDir(dataDir), "gui.tar.bz2")) 484 if err != nil { 485 return errors.Annotate(err, "cannot read GUI archive") 486 } 487 defer f.Close() 488 if err := guistorage.Add(f, binarystorage.Metadata{ 489 Version: gui.Version.String(), 490 Size: gui.Size, 491 SHA256: gui.SHA256, 492 }); err != nil { 493 return errors.Annotate(err, "cannot store GUI archive") 494 } 495 if err = st.GUISetVersion(gui.Version); err != nil { 496 return errors.Annotate(err, "cannot set current GUI version") 497 } 498 return nil 499 } 500 501 // Override for testing. 502 var seriesFromVersion = series.VersionSeries 503 504 // saveCustomImageMetadata stores the custom image metadata to the database, 505 func (c *BootstrapCommand) saveCustomImageMetadata(st *state.State, env environs.Environ, imageMetadata []*imagemetadata.ImageMetadata) error { 506 logger.Debugf("saving custom image metadata") 507 return storeImageMetadataInState(st, env, "custom", simplestreams.CUSTOM_CLOUD_DATA, imageMetadata) 508 } 509 510 // storeImageMetadataInState writes image metadata into state store. 511 func storeImageMetadataInState(st *state.State, env environs.Environ, source string, priority int, existingMetadata []*imagemetadata.ImageMetadata) error { 512 if len(existingMetadata) == 0 { 513 return nil 514 } 515 cfg := env.Config() 516 metadataState := make([]cloudimagemetadata.Metadata, len(existingMetadata)) 517 for i, one := range existingMetadata { 518 m := cloudimagemetadata.Metadata{ 519 MetadataAttributes: cloudimagemetadata.MetadataAttributes{ 520 Stream: one.Stream, 521 Region: one.RegionName, 522 Arch: one.Arch, 523 VirtType: one.VirtType, 524 RootStorageType: one.Storage, 525 Source: source, 526 Version: one.Version, 527 }, 528 Priority: priority, 529 ImageId: one.Id, 530 } 531 s, err := seriesFromVersion(one.Version) 532 if err != nil { 533 return errors.Annotatef(err, "cannot determine series for version %v", one.Version) 534 } 535 m.Series = s 536 if m.Stream == "" { 537 m.Stream = cfg.ImageStream() 538 } 539 if m.Source == "" { 540 m.Source = "custom" 541 } 542 metadataState[i] = m 543 } 544 if err := st.CloudImageMetadataStorage.SaveMetadataNoExpiry(metadataState); err != nil { 545 return errors.Annotatef(err, "cannot cache image metadata") 546 } 547 return nil 548 }