github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/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/state" 24 "github.com/juju/juju/state/api" 25 "github.com/juju/juju/state/api/params" 26 "github.com/juju/juju/tools" 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 defer func() { 75 if machineId != "" && err != nil { 76 logger.Errorf("provisioning failed, removing machine %v: %v", machineId, err) 77 if cleanupErr := client.DestroyMachines(machineId); cleanupErr != nil { 78 logger.Warningf("error cleaning up machine: %s", cleanupErr) 79 } 80 machineId = "" 81 } 82 client.Close() 83 }() 84 85 // Create the "ubuntu" user and initialise passwordless sudo. We populate 86 // the ubuntu user's authorized_keys file with the public keys in the current 87 // user's ~/.ssh directory. The authenticationworker will later update the 88 // ubuntu user's authorized_keys. 89 user, hostname := splitUserHost(args.Host) 90 authorizedKeys, err := config.ReadAuthorizedKeys("") 91 if err := InitUbuntuUser(hostname, user, authorizedKeys, args.Stdin, args.Stdout); err != nil { 92 return "", err 93 } 94 95 machineParams, err := gatherMachineParams(hostname) 96 if err != nil { 97 return "", err 98 } 99 100 // Inform Juju that the machine exists. 101 machineId, err = recordMachineInState(client, *machineParams) 102 if err != nil { 103 return "", err 104 } 105 106 provisioningScript, err := client.ProvisioningScript(params.ProvisioningScriptParams{ 107 MachineId: machineId, 108 Nonce: machineParams.Nonce, 109 }) 110 if err != nil { 111 return "", err 112 } 113 114 // Finally, provision the machine agent. 115 err = runProvisionScript(provisioningScript, hostname, args.Stderr) 116 if err != nil { 117 return machineId, err 118 } 119 120 logger.Infof("Provisioned machine %v", machineId) 121 return machineId, nil 122 } 123 124 func splitUserHost(host string) (string, string) { 125 if at := strings.Index(host, "@"); at != -1 { 126 return host[:at], host[at+1:] 127 } 128 return "", host 129 } 130 131 func recordMachineInState( 132 client *api.Client, machineParams params.AddMachineParams) (machineId string, err error) { 133 // Note: we explicitly use AddMachines1dot18 rather than AddMachines to preserve 134 // backwards compatibility; we do not require any of the new features of AddMachines 135 // here. 136 results, err := client.AddMachines1dot18([]params.AddMachineParams{machineParams}) 137 if err != nil { 138 return "", err 139 } 140 // Currently, only one machine is added, but in future there may be several added in one call. 141 machineInfo := results[0] 142 if machineInfo.Error != nil { 143 return "", machineInfo.Error 144 } 145 return machineInfo.Machine, nil 146 } 147 148 // convertToStateJobs takes a slice of params.MachineJob and makes them a slice of state.MachineJob 149 func convertToStateJobs(jobs []params.MachineJob) ([]state.MachineJob, error) { 150 outJobs := make([]state.MachineJob, len(jobs)) 151 var err error 152 for j, job := range jobs { 153 if outJobs[j], err = state.MachineJobFromParams(job); err != nil { 154 return nil, err 155 } 156 } 157 return outJobs, nil 158 } 159 160 // gatherMachineParams collects all the information we know about the machine 161 // we are about to provision. It will SSH into that machine as the ubuntu user. 162 // The hostname supplied should not include a username. 163 // If we can, we will reverse lookup the hostname by its IP address, and use 164 // the DNS resolved name, rather than the name that was supplied 165 func gatherMachineParams(hostname string) (*params.AddMachineParams, error) { 166 167 // Generate a unique nonce for the machine. 168 uuid, err := utils.NewUUID() 169 if err != nil { 170 return nil, err 171 } 172 173 var addrs []instance.Address 174 if addr, err := HostAddress(hostname); err != nil { 175 logger.Warningf("failed to compute public address for %q: %v", hostname, err) 176 } else { 177 addrs = append(addrs, addr) 178 } 179 180 provisioned, err := checkProvisioned(hostname) 181 if err != nil { 182 err = fmt.Errorf("error checking if provisioned: %v", err) 183 return nil, err 184 } 185 if provisioned { 186 return nil, ErrProvisioned 187 } 188 189 hc, series, err := DetectSeriesAndHardwareCharacteristics(hostname) 190 if err != nil { 191 err = fmt.Errorf("error detecting hardware characteristics: %v", err) 192 return nil, err 193 } 194 195 // There will never be a corresponding "instance" that any provider 196 // knows about. This is fine, and works well with the provisioner 197 // task. The provisioner task will happily remove any and all dead 198 // machines from state, but will ignore the associated instance ID 199 // if it isn't one that the environment provider knows about. 200 201 instanceId := instance.Id(manualInstancePrefix + hostname) 202 nonce := fmt.Sprintf("%s:%s", instanceId, uuid.String()) 203 machineParams := ¶ms.AddMachineParams{ 204 Series: series, 205 HardwareCharacteristics: hc, 206 InstanceId: instanceId, 207 Nonce: nonce, 208 Addrs: addrs, 209 Jobs: []params.MachineJob{params.JobHostUnits}, 210 } 211 return machineParams, nil 212 } 213 214 var provisionMachineAgent = func(host string, mcfg *cloudinit.MachineConfig, progressWriter io.Writer) error { 215 script, err := ProvisioningScript(mcfg) 216 if err != nil { 217 return err 218 } 219 return runProvisionScript(script, host, progressWriter) 220 } 221 222 // ProvisioningScript generates a bash script that can be 223 // executed on a remote host to carry out the cloud-init 224 // configuration. 225 func ProvisioningScript(mcfg *cloudinit.MachineConfig) (string, error) { 226 cloudcfg := coreCloudinit.New() 227 if err := cloudinit.ConfigureJuju(mcfg, cloudcfg); err != nil { 228 return "", err 229 } 230 // Explicitly disabling apt_upgrade so as not to trample 231 // the target machine's existing configuration. 232 cloudcfg.SetAptUpgrade(false) 233 configScript, err := sshinit.ConfigureScript(cloudcfg) 234 if err != nil { 235 return "", err 236 } 237 238 var buf bytes.Buffer 239 // Always remove the cloud-init-output.log file first, if it exists. 240 fmt.Fprintf(&buf, "rm -f %s\n", utils.ShQuote(mcfg.CloudInitOutputLog)) 241 // If something goes wrong, dump cloud-init-output.log to stderr. 242 buf.WriteString(shell.DumpFileOnErrorScript(mcfg.CloudInitOutputLog)) 243 buf.WriteString(configScript) 244 return buf.String(), nil 245 } 246 247 func runProvisionScript(script, host string, progressWriter io.Writer) error { 248 params := sshinit.ConfigureParams{ 249 Host: "ubuntu@" + host, 250 ProgressWriter: progressWriter, 251 } 252 return sshinit.RunConfigureScript(script, params) 253 }