github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/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 "bytes" 8 "errors" 9 "fmt" 10 "io" 11 "strings" 12 13 "github.com/juju/loggo" 14 "github.com/juju/utils" 15 "github.com/juju/utils/shell" 16 17 coreCloudinit "github.com/juju/juju/cloudinit" 18 "github.com/juju/juju/cloudinit/sshinit" 19 "github.com/juju/juju/environs/cloudinit" 20 "github.com/juju/juju/environs/config" 21 "github.com/juju/juju/instance" 22 "github.com/juju/juju/juju" 23 "github.com/juju/juju/network" 24 "github.com/juju/juju/state" 25 "github.com/juju/juju/state/api" 26 "github.com/juju/juju/state/api/params" 27 "github.com/juju/juju/tools" 28 ) 29 30 const manualInstancePrefix = "manual:" 31 32 var logger = loggo.GetLogger("juju.environs.manual") 33 34 type ProvisionMachineArgs struct { 35 // Host is the SSH host: [user@]host 36 Host string 37 38 // DataDir is the root directory for juju data. 39 // If left blank, the default location "/var/lib/juju" will be used. 40 DataDir string 41 42 // EnvName is the name of the environment for which the machine will be provisioned. 43 EnvName string 44 45 // Tools to install on the machine. If nil, tools will be automatically 46 // chosen using environs/tools FindInstanceTools. 47 Tools *tools.Tools 48 49 // Stdin is required to respond to sudo prompts, 50 // and must be a terminal (except in tests) 51 Stdin io.Reader 52 53 // Stdout is required to present sudo prompts to the user. 54 Stdout io.Writer 55 56 // Stderr is required to present machine provisioning progress to the user. 57 Stderr io.Writer 58 } 59 60 // ErrProvisioned is returned by ProvisionMachine if the target 61 // machine has an existing machine agent. 62 var ErrProvisioned = errors.New("machine is already provisioned") 63 64 // ProvisionMachine provisions a machine agent to an existing host, via 65 // an SSH connection to the specified host. The host may optionally be preceded 66 // with a login username, as in [user@]host. 67 // 68 // On successful completion, this function will return the id of the state.Machine 69 // that was entered into state. 70 func ProvisionMachine(args ProvisionMachineArgs) (machineId string, err error) { 71 client, err := juju.NewAPIClientFromName(args.EnvName) 72 if err != nil { 73 return "", err 74 } 75 defer func() { 76 if machineId != "" && err != nil { 77 logger.Errorf("provisioning failed, removing machine %v: %v", machineId, err) 78 if cleanupErr := client.DestroyMachines(machineId); cleanupErr != nil { 79 logger.Warningf("error cleaning up machine: %s", cleanupErr) 80 } 81 machineId = "" 82 } 83 client.Close() 84 }() 85 86 // Create the "ubuntu" user and initialise passwordless sudo. We populate 87 // the ubuntu user's authorized_keys file with the public keys in the current 88 // user's ~/.ssh directory. The authenticationworker will later update the 89 // ubuntu user's authorized_keys. 90 user, hostname := splitUserHost(args.Host) 91 authorizedKeys, err := config.ReadAuthorizedKeys("") 92 if err := InitUbuntuUser(hostname, user, authorizedKeys, args.Stdin, args.Stdout); err != nil { 93 return "", err 94 } 95 96 machineParams, err := gatherMachineParams(hostname) 97 if err != nil { 98 return "", err 99 } 100 101 // Inform Juju that the machine exists. 102 machineId, err = recordMachineInState(client, *machineParams) 103 if err != nil { 104 return "", err 105 } 106 107 provisioningScript, err := client.ProvisioningScript(params.ProvisioningScriptParams{ 108 MachineId: machineId, 109 Nonce: machineParams.Nonce, 110 }) 111 if err != nil { 112 return "", err 113 } 114 115 // Finally, provision the machine agent. 116 err = runProvisionScript(provisioningScript, hostname, args.Stderr) 117 if err != nil { 118 return machineId, err 119 } 120 121 logger.Infof("Provisioned machine %v", machineId) 122 return machineId, nil 123 } 124 125 func splitUserHost(host string) (string, string) { 126 if at := strings.Index(host, "@"); at != -1 { 127 return host[:at], host[at+1:] 128 } 129 return "", host 130 } 131 132 func recordMachineInState( 133 client *api.Client, machineParams params.AddMachineParams) (machineId string, err error) { 134 // Note: we explicitly use AddMachines1dot18 rather than AddMachines to preserve 135 // backwards compatibility; we do not require any of the new features of AddMachines 136 // here. 137 results, err := client.AddMachines1dot18([]params.AddMachineParams{machineParams}) 138 if err != nil { 139 return "", err 140 } 141 // Currently, only one machine is added, but in future there may be several added in one call. 142 machineInfo := results[0] 143 if machineInfo.Error != nil { 144 return "", machineInfo.Error 145 } 146 return machineInfo.Machine, nil 147 } 148 149 // convertToStateJobs takes a slice of params.MachineJob and makes them a slice of state.MachineJob 150 func convertToStateJobs(jobs []params.MachineJob) ([]state.MachineJob, error) { 151 outJobs := make([]state.MachineJob, len(jobs)) 152 var err error 153 for j, job := range jobs { 154 if outJobs[j], err = state.MachineJobFromParams(job); err != nil { 155 return nil, err 156 } 157 } 158 return outJobs, nil 159 } 160 161 // gatherMachineParams collects all the information we know about the machine 162 // we are about to provision. It will SSH into that machine as the ubuntu user. 163 // The hostname supplied should not include a username. 164 // If we can, we will reverse lookup the hostname by its IP address, and use 165 // the DNS resolved name, rather than the name that was supplied 166 func gatherMachineParams(hostname string) (*params.AddMachineParams, error) { 167 168 // Generate a unique nonce for the machine. 169 uuid, err := utils.NewUUID() 170 if err != nil { 171 return nil, err 172 } 173 174 var addrs []network.Address 175 if addr, err := HostAddress(hostname); err != nil { 176 logger.Warningf("failed to compute public address for %q: %v", hostname, err) 177 } else { 178 addrs = append(addrs, addr) 179 } 180 181 provisioned, err := checkProvisioned(hostname) 182 if err != nil { 183 err = fmt.Errorf("error checking if provisioned: %v", err) 184 return nil, err 185 } 186 if provisioned { 187 return nil, ErrProvisioned 188 } 189 190 hc, series, err := DetectSeriesAndHardwareCharacteristics(hostname) 191 if err != nil { 192 err = fmt.Errorf("error detecting hardware characteristics: %v", err) 193 return nil, err 194 } 195 196 // There will never be a corresponding "instance" that any provider 197 // knows about. This is fine, and works well with the provisioner 198 // task. The provisioner task will happily remove any and all dead 199 // machines from state, but will ignore the associated instance ID 200 // if it isn't one that the environment provider knows about. 201 202 instanceId := instance.Id(manualInstancePrefix + hostname) 203 nonce := fmt.Sprintf("%s:%s", instanceId, uuid.String()) 204 machineParams := ¶ms.AddMachineParams{ 205 Series: series, 206 HardwareCharacteristics: hc, 207 InstanceId: instanceId, 208 Nonce: nonce, 209 Addrs: addrs, 210 Jobs: []params.MachineJob{params.JobHostUnits}, 211 } 212 return machineParams, nil 213 } 214 215 var provisionMachineAgent = func(host string, mcfg *cloudinit.MachineConfig, progressWriter io.Writer) error { 216 script, err := ProvisioningScript(mcfg) 217 if err != nil { 218 return err 219 } 220 return runProvisionScript(script, host, progressWriter) 221 } 222 223 // ProvisioningScript generates a bash script that can be 224 // executed on a remote host to carry out the cloud-init 225 // configuration. 226 func ProvisioningScript(mcfg *cloudinit.MachineConfig) (string, error) { 227 cloudcfg := coreCloudinit.New() 228 if err := cloudinit.ConfigureJuju(mcfg, cloudcfg); err != nil { 229 return "", err 230 } 231 // Explicitly disabling apt_upgrade so as not to trample 232 // the target machine's existing configuration. 233 cloudcfg.SetAptUpgrade(false) 234 configScript, err := sshinit.ConfigureScript(cloudcfg) 235 if err != nil { 236 return "", err 237 } 238 239 var buf bytes.Buffer 240 // Always remove the cloud-init-output.log file first, if it exists. 241 fmt.Fprintf(&buf, "rm -f %s\n", utils.ShQuote(mcfg.CloudInitOutputLog)) 242 // If something goes wrong, dump cloud-init-output.log to stderr. 243 buf.WriteString(shell.DumpFileOnErrorScript(mcfg.CloudInitOutputLog)) 244 buf.WriteString(configScript) 245 return buf.String(), nil 246 } 247 248 func runProvisionScript(script, host string, progressWriter io.Writer) error { 249 params := sshinit.ConfigureParams{ 250 Host: "ubuntu@" + host, 251 ProgressWriter: progressWriter, 252 } 253 return sshinit.RunConfigureScript(script, params) 254 }