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