launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/environs/manual/provisioner.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package manual 5 6 import ( 7 "errors" 8 "fmt" 9 "io" 10 "net" 11 "strings" 12 13 "github.com/loggo/loggo" 14 15 coreCloudinit "launchpad.net/juju-core/cloudinit" 16 "launchpad.net/juju-core/cloudinit/sshinit" 17 "launchpad.net/juju-core/environs/cloudinit" 18 "launchpad.net/juju-core/environs/config" 19 "launchpad.net/juju-core/instance" 20 "launchpad.net/juju-core/juju" 21 "launchpad.net/juju-core/state" 22 "launchpad.net/juju-core/state/api" 23 "launchpad.net/juju-core/state/api/params" 24 "launchpad.net/juju-core/state/statecmd" 25 "launchpad.net/juju-core/tools" 26 "launchpad.net/juju-core/utils" 27 ) 28 29 const manualInstancePrefix = "manual:" 30 31 var logger = loggo.GetLogger("juju.environs.manual") 32 33 type ProvisionMachineArgs struct { 34 // Host is the SSH host: [user@]host 35 Host string 36 37 // DataDir is the root directory for juju data. 38 // If left blank, the default location "/var/lib/juju" will be used. 39 DataDir string 40 41 // EnvName is the name of the environment for which the machine will be provisioned. 42 EnvName string 43 44 // Tools to install on the machine. If nil, tools will be automatically 45 // chosen using environs/tools FindInstanceTools. 46 Tools *tools.Tools 47 48 // Stdin is required to respond to sudo prompts, 49 // and must be a terminal (except in tests) 50 Stdin io.Reader 51 52 // Stdout is required to present sudo prompts to the user. 53 Stdout io.Writer 54 55 // Stderr is required to present machine provisioning progress to the user. 56 Stderr io.Writer 57 } 58 59 // ErrProvisioned is returned by ProvisionMachine if the target 60 // machine has an existing machine agent. 61 var ErrProvisioned = errors.New("machine is already provisioned") 62 63 // ProvisionMachine provisions a machine agent to an existing host, via 64 // an SSH connection to the specified host. The host may optionally be preceded 65 // with a login username, as in [user@]host. 66 // 67 // On successful completion, this function will return the id of the state.Machine 68 // that was entered into state. 69 func ProvisionMachine(args ProvisionMachineArgs) (machineId string, err error) { 70 client, err := juju.NewAPIClientFromName(args.EnvName) 71 if err != nil { 72 return "", err 73 } 74 // Used for fallback to 1.16 code 75 var stateConn *juju.Conn 76 defer func() { 77 if machineId != "" && err != nil { 78 logger.Errorf("provisioning failed, removing machine %v: %v", machineId, err) 79 // If we have stateConn, then we are in 1.16 80 // compatibility mode and we should issue 81 // DestroyMachines directly on the state, rather than 82 // via API (because DestroyMachine *also* didn't exist 83 // in 1.16, though it will be in 1.16.5). 84 // TODO: When this compatibility code is removed, we 85 // should remove the method in state as well (as long 86 // as destroy-machine also no longer needs it.) 87 var cleanupErr error 88 if stateConn != nil { 89 cleanupErr = statecmd.DestroyMachines1dot16(stateConn.State, machineId) 90 } else { 91 cleanupErr = client.DestroyMachines(machineId) 92 } 93 if cleanupErr != nil { 94 logger.Warningf("error cleaning up machine: %s", cleanupErr) 95 } 96 machineId = "" 97 } 98 if stateConn != nil { 99 stateConn.Close() 100 stateConn = nil 101 } 102 client.Close() 103 }() 104 105 // Create the "ubuntu" user and initialise passwordless sudo. We populate 106 // the ubuntu user's authorized_keys file with the public keys in the current 107 // user's ~/.ssh directory. The authenticationworker will later update the 108 // ubuntu user's authorized_keys. 109 user, hostname := splitUserHost(args.Host) 110 authorizedKeys, err := config.ReadAuthorizedKeys("") 111 if err := InitUbuntuUser(hostname, user, authorizedKeys, args.Stdin, args.Stdout); err != nil { 112 return "", err 113 } 114 115 machineParams, err := gatherMachineParams(hostname) 116 if err != nil { 117 return "", err 118 } 119 120 // Inform Juju that the machine exists. 121 machineId, err = recordMachineInState(client, *machineParams) 122 if params.IsCodeNotImplemented(err) { 123 logger.Infof("AddMachines not supported by the API server, " + 124 "falling back to 1.16 compatibility mode (direct DB access)") 125 stateConn, err = juju.NewConnFromName(args.EnvName) 126 if err == nil { 127 machineId, err = recordMachineInState1dot16(stateConn, *machineParams) 128 } 129 } 130 if err != nil { 131 return "", err 132 } 133 134 var provisioningScript string 135 if stateConn == nil { 136 provisioningScript, err = client.ProvisioningScript(params.ProvisioningScriptParams{ 137 MachineId: machineId, 138 Nonce: machineParams.Nonce, 139 }) 140 if err != nil { 141 return "", err 142 } 143 } else { 144 mcfg, err := statecmd.MachineConfig(stateConn.State, machineId, machineParams.Nonce, args.DataDir) 145 if err == nil { 146 provisioningScript, err = generateProvisioningScript(mcfg) 147 } 148 if err != nil { 149 return "", err 150 } 151 } 152 153 // Finally, provision the machine agent. 154 err = runProvisionScript(provisioningScript, hostname, args.Stderr) 155 if err != nil { 156 return machineId, err 157 } 158 159 logger.Infof("Provisioned machine %v", machineId) 160 return machineId, nil 161 } 162 163 func splitUserHost(host string) (string, string) { 164 if at := strings.Index(host, "@"); at != -1 { 165 return host[:at], host[at+1:] 166 } 167 return "", host 168 } 169 170 func recordMachineInState( 171 client *api.Client, machineParams params.AddMachineParams) (machineId string, err error) { 172 results, err := client.AddMachines([]params.AddMachineParams{machineParams}) 173 if err != nil { 174 return "", err 175 } 176 // Currently, only one machine is added, but in future there may be several added in one call. 177 machineInfo := results[0] 178 if machineInfo.Error != nil { 179 return "", machineInfo.Error 180 } 181 return machineInfo.Machine, nil 182 } 183 184 // convertToStateJobs takes a slice of params.MachineJob and makes them a slice of state.MachineJob 185 func convertToStateJobs(jobs []params.MachineJob) ([]state.MachineJob, error) { 186 outJobs := make([]state.MachineJob, len(jobs)) 187 var err error 188 for j, job := range jobs { 189 if outJobs[j], err = state.MachineJobFromParams(job); err != nil { 190 return nil, err 191 } 192 } 193 return outJobs, nil 194 } 195 196 func recordMachineInState1dot16( 197 stateConn *juju.Conn, machineParams params.AddMachineParams) (machineId string, err error) { 198 stateJobs, err := convertToStateJobs(machineParams.Jobs) 199 if err != nil { 200 return "", err 201 } 202 //if p.Series == "" { 203 // p.Series = defaultSeries 204 //} 205 template := state.MachineTemplate{ 206 Series: machineParams.Series, 207 Constraints: machineParams.Constraints, 208 InstanceId: machineParams.InstanceId, 209 Jobs: stateJobs, 210 Nonce: machineParams.Nonce, 211 HardwareCharacteristics: machineParams.HardwareCharacteristics, 212 Addresses: machineParams.Addrs, 213 } 214 machine, err := stateConn.State.AddOneMachine(template) 215 if err != nil { 216 return "", err 217 } 218 return machine.Id(), nil 219 } 220 221 // gatherMachineParams collects all the information we know about the machine 222 // we are about to provision. It will SSH into that machine as the ubuntu user. 223 // The hostname supplied should not include a username. 224 // If we can, we will reverse lookup the hostname by its IP address, and use 225 // the DNS resolved name, rather than the name that was supplied 226 func gatherMachineParams(hostname string) (*params.AddMachineParams, error) { 227 228 // Generate a unique nonce for the machine. 229 uuid, err := utils.NewUUID() 230 if err != nil { 231 return nil, err 232 } 233 // First, gather the parameters needed to inject the existing host into state. 234 if ip := net.ParseIP(hostname); ip != nil { 235 // Do a reverse-lookup on the IP. The IP may not have 236 // a DNS entry, so just log a warning if this fails. 237 names, err := net.LookupAddr(ip.String()) 238 if err != nil { 239 logger.Infof("failed to resolve %v: %v", ip, err) 240 } else { 241 logger.Infof("resolved %v to %v", ip, names) 242 hostname = names[0] 243 // TODO: jam 2014-01-09 https://bugs.launchpad.net/bugs/1267387 244 // We change what 'hostname' we are using here (rather 245 // than an IP address we use the DNS name). I'm not 246 // sure why that is better, but if we are changing the 247 // host, we should probably be returning the hostname 248 // to the parent function. 249 // Also, we don't seem to try and compare if 'ip' is in 250 // the list of addrs returned from 251 // instance.HostAddresses in case you might get 252 // multiple and one of them is what you are supposed to 253 // be using. 254 } 255 } 256 addrs, err := HostAddresses(hostname) 257 if err != nil { 258 return nil, err 259 } 260 logger.Infof("addresses for %v: %v", hostname, addrs) 261 262 provisioned, err := checkProvisioned(hostname) 263 if err != nil { 264 err = fmt.Errorf("error checking if provisioned: %v", err) 265 return nil, err 266 } 267 if provisioned { 268 return nil, ErrProvisioned 269 } 270 271 hc, series, err := DetectSeriesAndHardwareCharacteristics(hostname) 272 if err != nil { 273 err = fmt.Errorf("error detecting hardware characteristics: %v", err) 274 return nil, err 275 } 276 277 // There will never be a corresponding "instance" that any provider 278 // knows about. This is fine, and works well with the provisioner 279 // task. The provisioner task will happily remove any and all dead 280 // machines from state, but will ignore the associated instance ID 281 // if it isn't one that the environment provider knows about. 282 283 instanceId := instance.Id(manualInstancePrefix + hostname) 284 nonce := fmt.Sprintf("%s:%s", instanceId, uuid.String()) 285 machineParams := ¶ms.AddMachineParams{ 286 Series: series, 287 HardwareCharacteristics: hc, 288 InstanceId: instanceId, 289 Nonce: nonce, 290 Addrs: addrs, 291 Jobs: []params.MachineJob{params.JobHostUnits}, 292 } 293 return machineParams, nil 294 } 295 296 func provisionMachineAgent(host string, mcfg *cloudinit.MachineConfig, progressWriter io.Writer) error { 297 script, err := generateProvisioningScript(mcfg) 298 if err != nil { 299 return err 300 } 301 return runProvisionScript(script, host, progressWriter) 302 } 303 304 func generateProvisioningScript(mcfg *cloudinit.MachineConfig) (string, error) { 305 cloudcfg := coreCloudinit.New() 306 if err := cloudinit.ConfigureJuju(mcfg, cloudcfg); err != nil { 307 return "", err 308 } 309 // Explicitly disabling apt_upgrade so as not to trample 310 // the target machine's existing configuration. 311 cloudcfg.SetAptUpgrade(false) 312 return sshinit.ConfigureScript(cloudcfg) 313 } 314 315 func runProvisionScript(script, host string, progressWriter io.Writer) error { 316 params := sshinit.ConfigureParams{ 317 Host: "ubuntu@" + host, 318 ProgressWriter: progressWriter, 319 } 320 return sshinit.RunConfigureScript(script, params) 321 }