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