launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/provider/maas/environ.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package maas 5 6 import ( 7 "encoding/base64" 8 "fmt" 9 "net/url" 10 "strings" 11 "sync" 12 "time" 13 14 "launchpad.net/gomaasapi" 15 16 "launchpad.net/juju-core/agent" 17 "launchpad.net/juju-core/constraints" 18 "launchpad.net/juju-core/environs" 19 "launchpad.net/juju-core/environs/cloudinit" 20 "launchpad.net/juju-core/environs/config" 21 "launchpad.net/juju-core/environs/imagemetadata" 22 "launchpad.net/juju-core/environs/simplestreams" 23 "launchpad.net/juju-core/environs/storage" 24 envtools "launchpad.net/juju-core/environs/tools" 25 "launchpad.net/juju-core/instance" 26 "launchpad.net/juju-core/provider/common" 27 "launchpad.net/juju-core/state" 28 "launchpad.net/juju-core/state/api" 29 "launchpad.net/juju-core/tools" 30 "launchpad.net/juju-core/utils" 31 ) 32 33 const ( 34 // We're using v1.0 of the MAAS API. 35 apiVersion = "1.0" 36 ) 37 38 // A request may fail to due "eventual consistency" semantics, which 39 // should resolve fairly quickly. A request may also fail due to a slow 40 // state transition (for instance an instance taking a while to release 41 // a security group after termination). The former failure mode is 42 // dealt with by shortAttempt, the latter by LongAttempt. 43 var shortAttempt = utils.AttemptStrategy{ 44 Total: 5 * time.Second, 45 Delay: 200 * time.Millisecond, 46 } 47 48 type maasEnviron struct { 49 name string 50 51 // ecfgMutex protects the *Unlocked fields below. 52 ecfgMutex sync.Mutex 53 54 ecfgUnlocked *maasEnvironConfig 55 maasClientUnlocked *gomaasapi.MAASObject 56 storageUnlocked storage.Storage 57 } 58 59 var _ environs.Environ = (*maasEnviron)(nil) 60 var _ imagemetadata.SupportsCustomSources = (*maasEnviron)(nil) 61 var _ envtools.SupportsCustomSources = (*maasEnviron)(nil) 62 63 func NewEnviron(cfg *config.Config) (*maasEnviron, error) { 64 env := new(maasEnviron) 65 err := env.SetConfig(cfg) 66 if err != nil { 67 return nil, err 68 } 69 env.name = cfg.Name() 70 env.storageUnlocked = NewStorage(env) 71 return env, nil 72 } 73 74 // Name is specified in the Environ interface. 75 func (env *maasEnviron) Name() string { 76 return env.name 77 } 78 79 // Bootstrap is specified in the Environ interface. 80 func (env *maasEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { 81 return common.Bootstrap(ctx, env, cons) 82 } 83 84 // StateInfo is specified in the Environ interface. 85 func (env *maasEnviron) StateInfo() (*state.Info, *api.Info, error) { 86 return common.StateInfo(env) 87 } 88 89 // ecfg returns the environment's maasEnvironConfig, and protects it with a 90 // mutex. 91 func (env *maasEnviron) ecfg() *maasEnvironConfig { 92 env.ecfgMutex.Lock() 93 defer env.ecfgMutex.Unlock() 94 return env.ecfgUnlocked 95 } 96 97 // Config is specified in the Environ interface. 98 func (env *maasEnviron) Config() *config.Config { 99 return env.ecfg().Config 100 } 101 102 // SetConfig is specified in the Environ interface. 103 func (env *maasEnviron) SetConfig(cfg *config.Config) error { 104 env.ecfgMutex.Lock() 105 defer env.ecfgMutex.Unlock() 106 107 // The new config has already been validated by itself, but now we 108 // validate the transition from the old config to the new. 109 var oldCfg *config.Config 110 if env.ecfgUnlocked != nil { 111 oldCfg = env.ecfgUnlocked.Config 112 } 113 cfg, err := env.Provider().Validate(cfg, oldCfg) 114 if err != nil { 115 return err 116 } 117 118 ecfg, err := providerInstance.newConfig(cfg) 119 if err != nil { 120 return err 121 } 122 123 env.ecfgUnlocked = ecfg 124 125 authClient, err := gomaasapi.NewAuthenticatedClient(ecfg.maasServer(), ecfg.maasOAuth(), apiVersion) 126 if err != nil { 127 return err 128 } 129 env.maasClientUnlocked = gomaasapi.NewMAAS(*authClient) 130 131 return nil 132 } 133 134 // getMAASClient returns a MAAS client object to use for a request, in a 135 // lock-protected fashion. 136 func (env *maasEnviron) getMAASClient() *gomaasapi.MAASObject { 137 env.ecfgMutex.Lock() 138 defer env.ecfgMutex.Unlock() 139 140 return env.maasClientUnlocked 141 } 142 143 // convertConstraints converts the given constraints into an url.Values 144 // object suitable to pass to MAAS when acquiring a node. 145 // CpuPower is ignored because it cannot translated into something 146 // meaningful for MAAS right now. 147 func convertConstraints(cons constraints.Value) url.Values { 148 params := url.Values{} 149 if cons.Arch != nil { 150 params.Add("arch", *cons.Arch) 151 } 152 if cons.CpuCores != nil { 153 params.Add("cpu_count", fmt.Sprintf("%d", *cons.CpuCores)) 154 } 155 if cons.Mem != nil { 156 params.Add("mem", fmt.Sprintf("%d", *cons.Mem)) 157 } 158 if cons.Tags != nil && len(*cons.Tags) > 0 { 159 params.Add("tags", strings.Join(*cons.Tags, ",")) 160 } 161 // TODO(bug 1212689): ignore root-disk constraint for now. 162 if cons.RootDisk != nil { 163 logger.Warningf("ignoring unsupported constraint 'root-disk'") 164 } 165 if cons.CpuPower != nil { 166 logger.Warningf("ignoring unsupported constraint 'cpu-power'") 167 } 168 return params 169 } 170 171 // acquireNode allocates a node from the MAAS. 172 func (environ *maasEnviron) acquireNode(cons constraints.Value, possibleTools tools.List) (gomaasapi.MAASObject, *tools.Tools, error) { 173 acquireParams := convertConstraints(cons) 174 acquireParams.Add("agent_name", environ.ecfg().maasAgentName()) 175 var result gomaasapi.JSONObject 176 var err error 177 for a := shortAttempt.Start(); a.Next(); { 178 client := environ.getMAASClient().GetSubObject("nodes/") 179 result, err = client.CallPost("acquire", acquireParams) 180 if err == nil { 181 break 182 } 183 } 184 if err != nil { 185 return gomaasapi.MAASObject{}, nil, err 186 } 187 node, err := result.GetMAASObject() 188 if err != nil { 189 msg := fmt.Errorf("unexpected result from 'acquire' on MAAS API: %v", err) 190 return gomaasapi.MAASObject{}, nil, msg 191 } 192 tools := possibleTools[0] 193 logger.Warningf("picked arbitrary tools %q", tools) 194 return node, tools, nil 195 } 196 197 // startNode installs and boots a node. 198 func (environ *maasEnviron) startNode(node gomaasapi.MAASObject, series string, userdata []byte) error { 199 userDataParam := base64.StdEncoding.EncodeToString(userdata) 200 params := url.Values{ 201 "distro_series": {series}, 202 "user_data": {userDataParam}, 203 } 204 // Initialize err to a non-nil value as a sentinel for the following 205 // loop. 206 err := fmt.Errorf("(no error)") 207 for a := shortAttempt.Start(); a.Next() && err != nil; { 208 _, err = node.CallPost("start", params) 209 } 210 return err 211 } 212 213 // createBridgeNetwork returns a string representing the upstart command to 214 // create a bridged eth0. 215 func createBridgeNetwork() string { 216 return `cat > /etc/network/eth0.config << EOF 217 iface eth0 inet manual 218 219 auto br0 220 iface br0 inet dhcp 221 bridge_ports eth0 222 EOF 223 ` 224 } 225 226 // linkBridgeInInterfaces adds the file created by createBridgeNetwork to the 227 // interfaces file. 228 func linkBridgeInInterfaces() string { 229 return `sed -i "s/iface eth0 inet dhcp/source \/etc\/network\/eth0.config/" /etc/network/interfaces` 230 } 231 232 // StartInstance is specified in the InstanceBroker interface. 233 func (environ *maasEnviron) StartInstance(cons constraints.Value, possibleTools tools.List, 234 machineConfig *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) { 235 236 var inst *maasInstance 237 var err error 238 if node, tools, err := environ.acquireNode(cons, possibleTools); err != nil { 239 return nil, nil, fmt.Errorf("cannot run instances: %v", err) 240 } else { 241 inst = &maasInstance{maasObject: &node, environ: environ} 242 machineConfig.Tools = tools 243 } 244 defer func() { 245 if err != nil { 246 if err := environ.releaseInstance(inst); err != nil { 247 logger.Errorf("error releasing failed instance: %v", err) 248 } 249 } 250 }() 251 252 hostname, err := inst.DNSName() 253 if err != nil { 254 return nil, nil, err 255 } 256 info := machineInfo{hostname} 257 runCmd, err := info.cloudinitRunCmd() 258 if err != nil { 259 return nil, nil, err 260 } 261 if err := environs.FinishMachineConfig(machineConfig, environ.Config(), cons); err != nil { 262 return nil, nil, err 263 } 264 // TODO(thumper): 2013-08-28 bug 1217614 265 // The machine envronment config values are being moved to the agent config. 266 // Explicitly specify that the lxc containers use the network bridge defined above. 267 machineConfig.AgentEnvironment[agent.LxcBridge] = "br0" 268 userdata, err := environs.ComposeUserData( 269 machineConfig, 270 runCmd, 271 createBridgeNetwork(), 272 linkBridgeInInterfaces(), 273 "service networking restart", 274 ) 275 if err != nil { 276 msg := fmt.Errorf("could not compose userdata for bootstrap node: %v", err) 277 return nil, nil, msg 278 } 279 logger.Debugf("maas user data; %d bytes", len(userdata)) 280 281 series := possibleTools.OneSeries() 282 if err := environ.startNode(*inst.maasObject, series, userdata); err != nil { 283 return nil, nil, err 284 } 285 logger.Debugf("started instance %q", inst.Id()) 286 // TODO(bug 1193998) - return instance hardware characteristics as well 287 return inst, nil, nil 288 } 289 290 // StartInstance is specified in the InstanceBroker interface. 291 func (environ *maasEnviron) StopInstances(instances []instance.Instance) error { 292 // Shortcut to exit quickly if 'instances' is an empty slice or nil. 293 if len(instances) == 0 { 294 return nil 295 } 296 // Tell MAAS to release each of the instances. If there are errors, 297 // return only the first one (but release all instances regardless). 298 // Note that releasing instances also turns them off. 299 var firstErr error 300 for _, instance := range instances { 301 err := environ.releaseInstance(instance) 302 if firstErr == nil { 303 firstErr = err 304 } 305 } 306 return firstErr 307 } 308 309 // releaseInstance releases a single instance. 310 func (environ *maasEnviron) releaseInstance(inst instance.Instance) error { 311 maasInst := inst.(*maasInstance) 312 maasObj := maasInst.maasObject 313 _, err := maasObj.CallPost("release", nil) 314 if err != nil { 315 logger.Debugf("error releasing instance %v", maasInst) 316 } 317 return err 318 } 319 320 // instances calls the MAAS API to list nodes. The "ids" slice is a filter for 321 // specific instance IDs. Due to how this works in the HTTP API, an empty 322 // "ids" matches all instances (not none as you might expect). 323 func (environ *maasEnviron) instances(ids []instance.Id) ([]instance.Instance, error) { 324 nodeListing := environ.getMAASClient().GetSubObject("nodes") 325 filter := getSystemIdValues(ids) 326 filter.Add("agent_name", environ.ecfg().maasAgentName()) 327 listNodeObjects, err := nodeListing.CallGet("list", filter) 328 if err != nil { 329 return nil, err 330 } 331 listNodes, err := listNodeObjects.GetArray() 332 if err != nil { 333 return nil, err 334 } 335 instances := make([]instance.Instance, len(listNodes)) 336 for index, nodeObj := range listNodes { 337 node, err := nodeObj.GetMAASObject() 338 if err != nil { 339 return nil, err 340 } 341 instances[index] = &maasInstance{ 342 maasObject: &node, 343 environ: environ, 344 } 345 } 346 return instances, nil 347 } 348 349 // Instances returns the instance.Instance objects corresponding to the given 350 // slice of instance.Id. The error is ErrNoInstances if no instances 351 // were found. 352 func (environ *maasEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) { 353 if len(ids) == 0 { 354 // This would be treated as "return all instances" below, so 355 // treat it as a special case. 356 // The interface requires us to return this particular error 357 // if no instances were found. 358 return nil, environs.ErrNoInstances 359 } 360 instances, err := environ.instances(ids) 361 if err != nil { 362 return nil, err 363 } 364 if len(instances) == 0 { 365 return nil, environs.ErrNoInstances 366 } 367 368 idMap := make(map[instance.Id]instance.Instance) 369 for _, instance := range instances { 370 idMap[instance.Id()] = instance 371 } 372 373 result := make([]instance.Instance, len(ids)) 374 for index, id := range ids { 375 result[index] = idMap[id] 376 } 377 378 if len(instances) < len(ids) { 379 return result, environs.ErrPartialInstances 380 } 381 return result, nil 382 } 383 384 // AllInstances returns all the instance.Instance in this provider. 385 func (environ *maasEnviron) AllInstances() ([]instance.Instance, error) { 386 return environ.instances(nil) 387 } 388 389 // Storage is defined by the Environ interface. 390 func (env *maasEnviron) Storage() storage.Storage { 391 env.ecfgMutex.Lock() 392 defer env.ecfgMutex.Unlock() 393 return env.storageUnlocked 394 } 395 396 func (environ *maasEnviron) Destroy() error { 397 return common.Destroy(environ) 398 } 399 400 // MAAS does not do firewalling so these port methods do nothing. 401 func (*maasEnviron) OpenPorts([]instance.Port) error { 402 logger.Debugf("unimplemented OpenPorts() called") 403 return nil 404 } 405 406 func (*maasEnviron) ClosePorts([]instance.Port) error { 407 logger.Debugf("unimplemented ClosePorts() called") 408 return nil 409 } 410 411 func (*maasEnviron) Ports() ([]instance.Port, error) { 412 logger.Debugf("unimplemented Ports() called") 413 return []instance.Port{}, nil 414 } 415 416 func (*maasEnviron) Provider() environs.EnvironProvider { 417 return &providerInstance 418 } 419 420 // GetImageSources returns a list of sources which are used to search for simplestreams image metadata. 421 func (e *maasEnviron) GetImageSources() ([]simplestreams.DataSource, error) { 422 // Add the simplestreams source off the control bucket. 423 return []simplestreams.DataSource{ 424 storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseImagesPath)}, nil 425 } 426 427 // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata. 428 func (e *maasEnviron) GetToolsSources() ([]simplestreams.DataSource, error) { 429 // Add the simplestreams source off the control bucket. 430 return []simplestreams.DataSource{ 431 storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseToolsPath)}, nil 432 }