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 := &params.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  }