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