github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/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 coreCloudinit "github.com/juju/juju/cloudinit" 19 "github.com/juju/juju/cloudinit/sshinit" 20 "github.com/juju/juju/environs/cloudinit" 21 "github.com/juju/juju/environs/config" 22 "github.com/juju/juju/instance" 23 "github.com/juju/juju/network" 24 "github.com/juju/juju/state/multiwatcher" 25 ) 26 27 const manualInstancePrefix = "manual:" 28 29 var logger = loggo.GetLogger("juju.environs.manual") 30 31 // ProvisioningClientAPI defines the methods that are needed for the manual 32 // provisioning of machines. An interface is used here to decouple the API 33 // consumer from the actual API implementation type. 34 type ProvisioningClientAPI interface { 35 AddMachines([]params.AddMachineParams) ([]params.AddMachinesResult, error) 36 ForceDestroyMachines(machines ...string) error 37 ProvisioningScript(params.ProvisioningScriptParams) (script string, err error) 38 } 39 40 type ProvisionMachineArgs struct { 41 // Host is the SSH host: [user@]host 42 Host string 43 44 // DataDir is the root directory for juju data. 45 // If left blank, the default location "/var/lib/juju" will be used. 46 DataDir string 47 48 // Client provides the API needed to provision the machines. 49 Client ProvisioningClientAPI 50 51 // Stdin is required to respond to sudo prompts, 52 // and must be a terminal (except in tests) 53 Stdin io.Reader 54 55 // Stdout is required to present sudo prompts to the user. 56 Stdout io.Writer 57 58 // Stderr is required to present machine provisioning progress to the user. 59 Stderr io.Writer 60 61 *params.UpdateBehavior 62 } 63 64 // ErrProvisioned is returned by ProvisionMachine if the target 65 // machine has an existing machine agent. 66 var ErrProvisioned = errors.New("machine is already provisioned") 67 68 // ProvisionMachine provisions a machine agent to an existing host, via 69 // an SSH connection to the specified host. The host may optionally be preceded 70 // with a login username, as in [user@]host. 71 // 72 // On successful completion, this function will return the id of the state.Machine 73 // that was entered into state. 74 func ProvisionMachine(args ProvisionMachineArgs) (machineId string, err error) { 75 defer func() { 76 if machineId != "" && err != nil { 77 logger.Errorf("provisioning failed, removing machine %v: %v", machineId, err) 78 if cleanupErr := args.Client.ForceDestroyMachines(machineId); cleanupErr != nil { 79 logger.Warningf("error cleaning up machine: %s", cleanupErr) 80 } 81 machineId = "" 82 } 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(args.Client, *machineParams) 102 if err != nil { 103 return "", err 104 } 105 106 provisioningScript, err := args.Client.ProvisioningScript(params.ProvisioningScriptParams{ 107 MachineId: machineId, 108 Nonce: machineParams.Nonce, 109 DisablePackageCommands: !args.EnableOSRefreshUpdate && !args.EnableOSUpgrade, 110 }) 111 112 if err != nil { 113 logger.Errorf("cannot obtain provisioning script") 114 return "", err 115 } 116 117 // Finally, provision the machine agent. 118 err = runProvisionScript(provisioningScript, hostname, args.Stderr) 119 if err != nil { 120 return machineId, err 121 } 122 123 logger.Infof("Provisioned machine %v", machineId) 124 return machineId, nil 125 } 126 127 func splitUserHost(host string) (string, string) { 128 if at := strings.Index(host, "@"); at != -1 { 129 return host[:at], host[at+1:] 130 } 131 return "", host 132 } 133 134 func recordMachineInState(client ProvisioningClientAPI, machineParams params.AddMachineParams) (machineId string, err error) { 135 results, err := client.AddMachines([]params.AddMachineParams{machineParams}) 136 if err != nil { 137 return "", err 138 } 139 // Currently, only one machine is added, but in future there may be several added in one call. 140 machineInfo := results[0] 141 if machineInfo.Error != nil { 142 return "", machineInfo.Error 143 } 144 return machineInfo.Machine, nil 145 } 146 147 // gatherMachineParams collects all the information we know about the machine 148 // we are about to provision. It will SSH into that machine as the ubuntu user. 149 // The hostname supplied should not include a username. 150 // If we can, we will reverse lookup the hostname by its IP address, and use 151 // the DNS resolved name, rather than the name that was supplied 152 func gatherMachineParams(hostname string) (*params.AddMachineParams, error) { 153 154 // Generate a unique nonce for the machine. 155 uuid, err := utils.NewUUID() 156 if err != nil { 157 return nil, err 158 } 159 160 var addrs []network.Address 161 if addr, err := HostAddress(hostname); err != nil { 162 logger.Warningf("failed to compute public address for %q: %v", hostname, err) 163 } else { 164 addrs = append(addrs, addr) 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 // Also, manually provisioned machines don't have the JobManageNetworking. 188 // This ensures that the networker is running in non-intrusive mode 189 // and never touches the network configuration files. 190 // No JobManageNetworking here due to manual provisioning. 191 192 instanceId := instance.Id(manualInstancePrefix + hostname) 193 nonce := fmt.Sprintf("%s:%s", instanceId, uuid.String()) 194 machineParams := ¶ms.AddMachineParams{ 195 Series: series, 196 HardwareCharacteristics: hc, 197 InstanceId: instanceId, 198 Nonce: nonce, 199 Addrs: addrs, 200 Jobs: []multiwatcher.MachineJob{multiwatcher.JobHostUnits}, 201 } 202 return machineParams, nil 203 } 204 205 var provisionMachineAgent = func(host string, mcfg *cloudinit.MachineConfig, progressWriter io.Writer) error { 206 script, err := ProvisioningScript(mcfg) 207 if err != nil { 208 return err 209 } 210 return runProvisionScript(script, host, progressWriter) 211 } 212 213 // ProvisioningScript generates a bash script that can be 214 // executed on a remote host to carry out the cloud-init 215 // configuration. 216 func ProvisioningScript(mcfg *cloudinit.MachineConfig) (string, error) { 217 218 cloudcfg := coreCloudinit.New() 219 cloudcfg.SetAptUpdate(mcfg.EnableOSRefreshUpdate) 220 cloudcfg.SetAptUpgrade(mcfg.EnableOSUpgrade) 221 222 udata, err := cloudinit.NewUserdataConfig(mcfg, cloudcfg) 223 if err != nil { 224 return "", errors.Annotate(err, "error generating cloud-config") 225 } 226 if err := udata.ConfigureJuju(); err != nil { 227 return "", errors.Annotate(err, "error generating cloud-config") 228 } 229 230 configScript, err := sshinit.ConfigureScript(cloudcfg) 231 if err != nil { 232 return "", errors.Annotate(err, "error converting cloud-config to script") 233 } 234 235 var buf bytes.Buffer 236 // Always remove the cloud-init-output.log file first, if it exists. 237 fmt.Fprintf(&buf, "rm -f %s\n", utils.ShQuote(mcfg.CloudInitOutputLog)) 238 // If something goes wrong, dump cloud-init-output.log to stderr. 239 buf.WriteString(shell.DumpFileOnErrorScript(mcfg.CloudInitOutputLog)) 240 buf.WriteString(configScript) 241 return buf.String(), nil 242 } 243 244 func runProvisionScript(script, host string, progressWriter io.Writer) error { 245 params := sshinit.ConfigureParams{ 246 Host: "ubuntu@" + host, 247 ProgressWriter: progressWriter, 248 } 249 return sshinit.RunConfigureScript(script, params) 250 }