github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/cloudconfig/instancecfg/instancecfg.go (about) 1 // Copyright 2012, 2013, 2015 Canonical Ltd. 2 // Copyright 2015 Cloudbase Solutions SRL 3 // Licensed under the AGPLv3, see LICENCE file for details. 4 5 package instancecfg 6 7 import ( 8 "fmt" 9 "net" 10 "path" 11 "strconv" 12 13 "github.com/juju/errors" 14 "github.com/juju/loggo" 15 "github.com/juju/names" 16 "github.com/juju/utils" 17 "github.com/juju/utils/proxy" 18 "github.com/juju/utils/shell" 19 20 "github.com/juju/juju/agent" 21 agenttools "github.com/juju/juju/agent/tools" 22 "github.com/juju/juju/api" 23 "github.com/juju/juju/apiserver/params" 24 "github.com/juju/juju/constraints" 25 "github.com/juju/juju/environs/config" 26 "github.com/juju/juju/environs/imagemetadata" 27 "github.com/juju/juju/environs/tags" 28 "github.com/juju/juju/instance" 29 "github.com/juju/juju/juju/paths" 30 "github.com/juju/juju/mongo" 31 "github.com/juju/juju/service" 32 "github.com/juju/juju/service/common" 33 "github.com/juju/juju/state/multiwatcher" 34 coretools "github.com/juju/juju/tools" 35 "github.com/juju/juju/version" 36 ) 37 38 var logger = loggo.GetLogger("juju.cloudconfig.instancecfg") 39 40 // InstanceConfig represents initialization information for a new juju instance. 41 type InstanceConfig struct { 42 // Tags is a set of tags to set on the instance, if supported. This 43 // should be populated using the InstanceTags method in this package. 44 Tags map[string]string 45 46 // Bootstrap specifies whether the new instance is the bootstrap 47 // instance. When this is true, StateServingInfo should be set 48 // and filled out. 49 Bootstrap bool 50 51 // StateServingInfo holds the information for serving the state. 52 // This must only be set if the Bootstrap field is true 53 // (state servers started subsequently will acquire their serving info 54 // from another server) 55 StateServingInfo *params.StateServingInfo 56 57 // MongoInfo holds the means for the new instance to communicate with the 58 // juju state database. Unless the new instance is running a state server 59 // (StateServer is set), there must be at least one state server address supplied. 60 // The entity name must match that of the instance being started, 61 // or be empty when starting a state server. 62 MongoInfo *mongo.MongoInfo 63 64 // APIInfo holds the means for the new instance to communicate with the 65 // juju state API. Unless the new instance is running a state server (StateServer is 66 // set), there must be at least one state server address supplied. 67 // The entity name must match that of the instance being started, 68 // or be empty when starting a state server. 69 APIInfo *api.Info 70 71 // InstanceId is the instance ID of the instance being initialised. 72 // This is required when bootstrapping, and ignored otherwise. 73 InstanceId instance.Id 74 75 // HardwareCharacteristics contains the harrdware characteristics of 76 // the instance being initialised. This optional, and is only used by 77 // the bootstrap agent during state initialisation. 78 HardwareCharacteristics *instance.HardwareCharacteristics 79 80 // MachineNonce is set at provisioning/bootstrap time and used to 81 // ensure the agent is running on the correct instance. 82 MachineNonce string 83 84 // Tools is juju tools to be used on the new instance. 85 Tools *coretools.Tools 86 87 // DataDir holds the directory that juju state will be put in the new 88 // instance. 89 DataDir string 90 91 // LogDir holds the directory that juju logs will be written to. 92 LogDir string 93 94 // Jobs holds what machine jobs to run. 95 Jobs []multiwatcher.MachineJob 96 97 // CloudInitOutputLog specifies the path to the output log for cloud-init. 98 // The directory containing the log file must already exist. 99 CloudInitOutputLog string 100 101 // MachineId identifies the new machine. 102 MachineId string 103 104 // MachineContainerType specifies the type of container that the instance 105 // is. If the instance is not a container, then the type is "". 106 MachineContainerType instance.ContainerType 107 108 // MachineContainerHostname specifies the hostname to be used with the 109 // cloud config for the instance. If this is not set, hostname uses the default. 110 MachineContainerHostname string 111 112 // Networks holds a list of networks the instances should be on. 113 // 114 // TODO(dimitern): Drop this in a follow-up in favor or spaces 115 // constraints. 116 Networks []string 117 118 // AuthorizedKeys specifies the keys that are allowed to 119 // connect to the instance (see cloudinit.SSHAddAuthorizedKeys) 120 // If no keys are supplied, there can be no ssh access to the node. 121 // On a bootstrap instance, that is fatal. On other 122 // instances it will mean that the ssh, scp and debug-hooks 123 // commands cannot work. 124 AuthorizedKeys string 125 126 // AgentEnvironment defines additional configuration variables to set in 127 // the instance agent config. 128 AgentEnvironment map[string]string 129 130 // WARNING: this is only set if the instance being configured is 131 // a state server node. 132 // 133 // Config holds the initial environment configuration. 134 Config *config.Config 135 136 // Constraints holds the initial environment constraints. 137 Constraints constraints.Value 138 139 // DisableSSLHostnameVerification can be set to true to tell cloud-init 140 // that it shouldn't verify SSL certificates 141 DisableSSLHostnameVerification bool 142 143 // Series represents the instance series. 144 Series string 145 146 // MachineAgentServiceName is the init service name for the Juju machine agent. 147 MachineAgentServiceName string 148 149 // ProxySettings define normal http, https and ftp proxies. 150 ProxySettings proxy.Settings 151 152 // AptProxySettings define the http, https and ftp proxy settings to use 153 // for apt, which may or may not be the same as the normal ProxySettings. 154 AptProxySettings proxy.Settings 155 156 // AptMirror defines an APT mirror location, which, if specified, will 157 // override the default APT sources. 158 AptMirror string 159 160 // PreferIPv6 mirrors the value of prefer-ipv6 environment setting 161 // and when set IPv6 addresses for connecting to the API/state 162 // servers will be preferred over IPv4 ones. 163 PreferIPv6 bool 164 165 // The type of Simple Stream to download and deploy on this instance. 166 ImageStream string 167 168 // CustomImageMetadata is optional custom simplestreams image metadata 169 // to store in environment storage at bootstrap time. This is ignored 170 // in non-bootstrap instances. 171 CustomImageMetadata []*imagemetadata.ImageMetadata 172 173 // EnableOSRefreshUpdate specifies whether Juju will refresh its 174 // respective OS's updates list. 175 EnableOSRefreshUpdate bool 176 177 // EnableOSUpgrade defines Juju's behavior when provisioning 178 // instances. If enabled, the OS will perform any upgrades 179 // available as part of its provisioning. 180 EnableOSUpgrade bool 181 } 182 183 func (cfg *InstanceConfig) agentInfo() service.AgentInfo { 184 return service.NewMachineAgentInfo( 185 cfg.MachineId, 186 cfg.DataDir, 187 cfg.LogDir, 188 ) 189 } 190 191 func (cfg *InstanceConfig) ToolsDir(renderer shell.Renderer) string { 192 return cfg.agentInfo().ToolsDir(renderer) 193 } 194 195 func (cfg *InstanceConfig) InitService(renderer shell.Renderer) (service.Service, error) { 196 conf := service.AgentConf(cfg.agentInfo(), renderer) 197 198 name := cfg.MachineAgentServiceName 199 svc, err := newService(name, conf, cfg.Series) 200 return svc, errors.Trace(err) 201 } 202 203 var newService = func(name string, conf common.Conf, series string) (service.Service, error) { 204 return service.NewService(name, conf, series) 205 } 206 207 func (cfg *InstanceConfig) AgentConfig( 208 tag names.Tag, 209 toolsVersion version.Number, 210 ) (agent.ConfigSetter, error) { 211 // TODO for HAState: the stateHostAddrs and apiHostAddrs here assume that 212 // if the instance is a stateServer then to use localhost. This may be 213 // sufficient, but needs thought in the new world order. 214 var password string 215 if cfg.MongoInfo == nil { 216 password = cfg.APIInfo.Password 217 } else { 218 password = cfg.MongoInfo.Password 219 } 220 configParams := agent.AgentConfigParams{ 221 DataDir: cfg.DataDir, 222 LogDir: cfg.LogDir, 223 Jobs: cfg.Jobs, 224 Tag: tag, 225 UpgradedToVersion: toolsVersion, 226 Password: password, 227 Nonce: cfg.MachineNonce, 228 StateAddresses: cfg.stateHostAddrs(), 229 APIAddresses: cfg.ApiHostAddrs(), 230 CACert: cfg.MongoInfo.CACert, 231 Values: cfg.AgentEnvironment, 232 PreferIPv6: cfg.PreferIPv6, 233 Environment: cfg.APIInfo.EnvironTag, 234 } 235 if !cfg.Bootstrap { 236 return agent.NewAgentConfig(configParams) 237 } 238 return agent.NewStateMachineConfig(configParams, *cfg.StateServingInfo) 239 } 240 241 func (cfg *InstanceConfig) JujuTools() string { 242 return agenttools.SharedToolsDir(cfg.DataDir, cfg.Tools.Version) 243 } 244 245 func (cfg *InstanceConfig) stateHostAddrs() []string { 246 var hosts []string 247 if cfg.Bootstrap { 248 if cfg.PreferIPv6 { 249 hosts = append(hosts, net.JoinHostPort("::1", strconv.Itoa(cfg.StateServingInfo.StatePort))) 250 } else { 251 hosts = append(hosts, net.JoinHostPort("localhost", strconv.Itoa(cfg.StateServingInfo.StatePort))) 252 } 253 } 254 if cfg.MongoInfo != nil { 255 hosts = append(hosts, cfg.MongoInfo.Addrs...) 256 } 257 return hosts 258 } 259 260 func (cfg *InstanceConfig) ApiHostAddrs() []string { 261 var hosts []string 262 if cfg.Bootstrap { 263 if cfg.PreferIPv6 { 264 hosts = append(hosts, net.JoinHostPort("::1", strconv.Itoa(cfg.StateServingInfo.APIPort))) 265 } else { 266 hosts = append(hosts, net.JoinHostPort("localhost", strconv.Itoa(cfg.StateServingInfo.APIPort))) 267 } 268 } 269 if cfg.APIInfo != nil { 270 hosts = append(hosts, cfg.APIInfo.Addrs...) 271 } 272 return hosts 273 } 274 275 // HasNetworks returns if there are any networks set. 276 func (cfg *InstanceConfig) HasNetworks() bool { 277 return len(cfg.Networks) > 0 || cfg.Constraints.HaveNetworks() 278 } 279 280 type requiresError string 281 282 func (e requiresError) Error() string { 283 return "invalid machine configuration: missing " + string(e) 284 } 285 286 func (cfg *InstanceConfig) VerifyConfig() (err error) { 287 defer errors.DeferredAnnotatef(&err, "invalid machine configuration") 288 if !names.IsValidMachine(cfg.MachineId) { 289 return errors.New("invalid machine id") 290 } 291 if cfg.DataDir == "" { 292 return errors.New("missing var directory") 293 } 294 if cfg.LogDir == "" { 295 return errors.New("missing log directory") 296 } 297 if len(cfg.Jobs) == 0 { 298 return errors.New("missing machine jobs") 299 } 300 if cfg.CloudInitOutputLog == "" { 301 return errors.New("missing cloud-init output log path") 302 } 303 if cfg.Tools == nil { 304 return errors.New("missing tools") 305 } 306 if cfg.Tools.URL == "" { 307 return errors.New("missing tools URL") 308 } 309 if cfg.MongoInfo == nil { 310 return errors.New("missing state info") 311 } 312 if len(cfg.MongoInfo.CACert) == 0 { 313 return errors.New("missing CA certificate") 314 } 315 if cfg.APIInfo == nil { 316 return errors.New("missing API info") 317 } 318 if cfg.APIInfo.EnvironTag.Id() == "" { 319 return errors.New("missing environment tag") 320 } 321 if len(cfg.APIInfo.CACert) == 0 { 322 return errors.New("missing API CA certificate") 323 } 324 if cfg.MachineAgentServiceName == "" { 325 return errors.New("missing machine agent service name") 326 } 327 if cfg.Bootstrap { 328 if cfg.Config == nil { 329 return errors.New("missing environment configuration") 330 } 331 if cfg.MongoInfo.Tag != nil { 332 return errors.New("entity tag must be nil when starting a state server") 333 } 334 if cfg.APIInfo.Tag != nil { 335 return errors.New("entity tag must be nil when starting a state server") 336 } 337 if cfg.StateServingInfo == nil { 338 return errors.New("missing state serving info") 339 } 340 if len(cfg.StateServingInfo.Cert) == 0 { 341 return errors.New("missing state server certificate") 342 } 343 if len(cfg.StateServingInfo.PrivateKey) == 0 { 344 return errors.New("missing state server private key") 345 } 346 if len(cfg.StateServingInfo.CAPrivateKey) == 0 { 347 return errors.New("missing ca cert private key") 348 } 349 if cfg.StateServingInfo.StatePort == 0 { 350 return errors.New("missing state port") 351 } 352 if cfg.StateServingInfo.APIPort == 0 { 353 return errors.New("missing API port") 354 } 355 if cfg.InstanceId == "" { 356 return errors.New("missing instance-id") 357 } 358 } else { 359 if len(cfg.MongoInfo.Addrs) == 0 { 360 return errors.New("missing state hosts") 361 } 362 if cfg.MongoInfo.Tag != names.NewMachineTag(cfg.MachineId) { 363 return errors.New("entity tag must match started machine") 364 } 365 if len(cfg.APIInfo.Addrs) == 0 { 366 return errors.New("missing API hosts") 367 } 368 if cfg.APIInfo.Tag != names.NewMachineTag(cfg.MachineId) { 369 return errors.New("entity tag must match started machine") 370 } 371 if cfg.StateServingInfo != nil { 372 return errors.New("state serving info unexpectedly present") 373 } 374 } 375 if cfg.MachineNonce == "" { 376 return errors.New("missing machine nonce") 377 } 378 return nil 379 } 380 381 // logDir returns a filesystem path to the location where applications 382 // may create a folder containing logs 383 var logDir = paths.MustSucceed(paths.LogDir(version.Current.Series)) 384 385 // DefaultBridgeName is the network bridge device name used for LXC and KVM 386 // containers 387 const DefaultBridgeName = "juju-br0" 388 389 // NewInstanceConfig sets up a basic machine configuration, for a 390 // non-bootstrap node. You'll still need to supply more information, 391 // but this takes care of the fixed entries and the ones that are 392 // always needed. 393 func NewInstanceConfig( 394 machineID, 395 machineNonce, 396 imageStream, 397 series string, 398 secureServerConnections bool, 399 networks []string, 400 mongoInfo *mongo.MongoInfo, 401 apiInfo *api.Info, 402 ) (*InstanceConfig, error) { 403 dataDir, err := paths.DataDir(series) 404 if err != nil { 405 return nil, err 406 } 407 logDir, err := paths.LogDir(series) 408 if err != nil { 409 return nil, err 410 } 411 cloudInitOutputLog := path.Join(logDir, "cloud-init-output.log") 412 icfg := &InstanceConfig{ 413 // Fixed entries. 414 DataDir: dataDir, 415 LogDir: path.Join(logDir, "juju"), 416 Jobs: []multiwatcher.MachineJob{multiwatcher.JobHostUnits}, 417 CloudInitOutputLog: cloudInitOutputLog, 418 MachineAgentServiceName: "jujud-" + names.NewMachineTag(machineID).String(), 419 Series: series, 420 Tags: map[string]string{}, 421 422 // Parameter entries. 423 MachineId: machineID, 424 MachineNonce: machineNonce, 425 Networks: networks, 426 MongoInfo: mongoInfo, 427 APIInfo: apiInfo, 428 ImageStream: imageStream, 429 AgentEnvironment: map[string]string{ 430 agent.AllowsSecureConnection: strconv.FormatBool(secureServerConnections), 431 }, 432 } 433 return icfg, nil 434 } 435 436 // NewBootstrapInstanceConfig sets up a basic machine configuration for a 437 // bootstrap node. You'll still need to supply more information, but this 438 // takes care of the fixed entries and the ones that are always needed. 439 func NewBootstrapInstanceConfig(cons constraints.Value, series string) (*InstanceConfig, error) { 440 // For a bootstrap instance, FinishInstanceConfig will provide the 441 // state.Info and the api.Info. The machine id must *always* be "0". 442 icfg, err := NewInstanceConfig("0", agent.BootstrapNonce, "", series, true, nil, nil, nil) 443 if err != nil { 444 return nil, err 445 } 446 icfg.Bootstrap = true 447 icfg.Jobs = []multiwatcher.MachineJob{ 448 multiwatcher.JobManageEnviron, 449 multiwatcher.JobHostUnits, 450 } 451 icfg.Constraints = cons 452 return icfg, nil 453 } 454 455 // PopulateInstanceConfig is called both from the FinishInstanceConfig below, 456 // which does have access to the environment config, and from the container 457 // provisioners, which don't have access to the environment config. Everything 458 // that is needed to provision a container needs to be returned to the 459 // provisioner in the ContainerConfig structure. Those values are then used to 460 // call this function. 461 func PopulateInstanceConfig(icfg *InstanceConfig, 462 providerType, authorizedKeys string, 463 sslHostnameVerification bool, 464 proxySettings, aptProxySettings proxy.Settings, 465 aptMirror string, 466 preferIPv6 bool, 467 enableOSRefreshUpdates bool, 468 enableOSUpgrade bool, 469 ) error { 470 if authorizedKeys == "" { 471 return fmt.Errorf("environment configuration has no authorized-keys") 472 } 473 icfg.AuthorizedKeys = authorizedKeys 474 if icfg.AgentEnvironment == nil { 475 icfg.AgentEnvironment = make(map[string]string) 476 } 477 icfg.AgentEnvironment[agent.ProviderType] = providerType 478 icfg.AgentEnvironment[agent.ContainerType] = string(icfg.MachineContainerType) 479 icfg.DisableSSLHostnameVerification = !sslHostnameVerification 480 icfg.ProxySettings = proxySettings 481 icfg.AptProxySettings = aptProxySettings 482 icfg.AptMirror = aptMirror 483 icfg.PreferIPv6 = preferIPv6 484 icfg.EnableOSRefreshUpdate = enableOSRefreshUpdates 485 icfg.EnableOSUpgrade = enableOSUpgrade 486 return nil 487 } 488 489 // FinishInstanceConfig sets fields on a InstanceConfig that can be determined by 490 // inspecting a plain config.Config and the machine constraints at the last 491 // moment before bootstrapping. It assumes that the supplied Config comes from 492 // an environment that has passed through all the validation checks in the 493 // Bootstrap func, and that has set an agent-version (via finding the tools to, 494 // use for bootstrap, or otherwise). 495 // TODO(fwereade) This function is not meant to be "good" in any serious way: 496 // it is better that this functionality be collected in one place here than 497 // that it be spread out across 3 or 4 providers, but this is its only 498 // redeeming feature. 499 func FinishInstanceConfig(icfg *InstanceConfig, cfg *config.Config) (err error) { 500 defer errors.DeferredAnnotatef(&err, "cannot complete machine configuration") 501 502 if err := PopulateInstanceConfig( 503 icfg, 504 cfg.Type(), 505 cfg.AuthorizedKeys(), 506 cfg.SSLHostnameVerification(), 507 cfg.ProxySettings(), 508 cfg.AptProxySettings(), 509 cfg.AptMirror(), 510 cfg.PreferIPv6(), 511 cfg.EnableOSRefreshUpdate(), 512 cfg.EnableOSUpgrade(), 513 ); err != nil { 514 return errors.Trace(err) 515 } 516 517 if isStateInstanceConfig(icfg) { 518 // Add NUMACTL preference. Needed to work for both bootstrap and high availability 519 // Only makes sense for state server 520 logger.Debugf("Setting numa ctl preference to %v", cfg.NumaCtlPreference()) 521 // Unfortunately, AgentEnvironment can only take strings as values 522 icfg.AgentEnvironment[agent.NumaCtlPreference] = fmt.Sprintf("%v", cfg.NumaCtlPreference()) 523 } 524 // The following settings are only appropriate at bootstrap time. At the 525 // moment, the only state server is the bootstrap node, but this 526 // will probably change. 527 if !icfg.Bootstrap { 528 return nil 529 } 530 if icfg.APIInfo != nil || icfg.MongoInfo != nil { 531 return errors.New("machine configuration already has api/state info") 532 } 533 caCert, hasCACert := cfg.CACert() 534 if !hasCACert { 535 return errors.New("environment configuration has no ca-cert") 536 } 537 password := cfg.AdminSecret() 538 if password == "" { 539 return errors.New("environment configuration has no admin-secret") 540 } 541 passwordHash := utils.UserPasswordHash(password, utils.CompatSalt) 542 envUUID, uuidSet := cfg.UUID() 543 if !uuidSet { 544 return errors.New("config missing environment uuid") 545 } 546 icfg.APIInfo = &api.Info{ 547 Password: passwordHash, 548 CACert: caCert, 549 EnvironTag: names.NewEnvironTag(envUUID), 550 } 551 icfg.MongoInfo = &mongo.MongoInfo{Password: passwordHash, Info: mongo.Info{CACert: caCert}} 552 553 // These really are directly relevant to running a state server. 554 // Initially, generate a state server certificate with no host IP 555 // addresses in the SAN field. Once the state server is up and the 556 // NIC addresses become known, the certificate can be regenerated. 557 cert, key, err := cfg.GenerateStateServerCertAndKey(nil) 558 if err != nil { 559 return errors.Annotate(err, "cannot generate state server certificate") 560 } 561 caPrivateKey, hasCAPrivateKey := cfg.CAPrivateKey() 562 if !hasCAPrivateKey { 563 return errors.New("environment configuration has no ca-private-key") 564 } 565 srvInfo := params.StateServingInfo{ 566 StatePort: cfg.StatePort(), 567 APIPort: cfg.APIPort(), 568 Cert: string(cert), 569 PrivateKey: string(key), 570 CAPrivateKey: caPrivateKey, 571 } 572 icfg.StateServingInfo = &srvInfo 573 if icfg.Config, err = bootstrapConfig(cfg); err != nil { 574 return errors.Trace(err) 575 } 576 577 return nil 578 } 579 580 // InstanceTags returns the minimum set of tags that should be set on a 581 // machine instance, if the provider supports them. 582 func InstanceTags(cfg *config.Config, jobs []multiwatcher.MachineJob) map[string]string { 583 uuid, _ := cfg.UUID() 584 instanceTags := tags.ResourceTags(names.NewEnvironTag(uuid), cfg) 585 if multiwatcher.AnyJobNeedsState(jobs...) { 586 instanceTags[tags.JujuStateServer] = "true" 587 } 588 return instanceTags 589 } 590 591 // bootstrapConfig returns a copy of the supplied configuration with the 592 // admin-secret and ca-private-key attributes removed. If the resulting 593 // config is not suitable for bootstrapping an environment, an error is 594 // returned. 595 // This function is copied from environs in here so we can avoid an import loop 596 func bootstrapConfig(cfg *config.Config) (*config.Config, error) { 597 m := cfg.AllAttrs() 598 // We never want to push admin-secret or the root CA private key to the cloud. 599 delete(m, "admin-secret") 600 delete(m, "ca-private-key") 601 cfg, err := config.New(config.NoDefaults, m) 602 if err != nil { 603 return nil, err 604 } 605 if _, ok := cfg.AgentVersion(); !ok { 606 return nil, fmt.Errorf("environment configuration has no agent-version") 607 } 608 return cfg, nil 609 } 610 611 // isStateInstanceConfig determines if given machine configuration 612 // is for State Server by iterating over machine's jobs. 613 // If JobManageEnviron is present, this is a state server. 614 func isStateInstanceConfig(icfg *InstanceConfig) bool { 615 for _, aJob := range icfg.Jobs { 616 if aJob == multiwatcher.JobManageEnviron { 617 return true 618 } 619 } 620 return false 621 }