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