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