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