github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/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  	coreCloudinit "github.com/juju/juju/cloudinit"
    19  	"github.com/juju/juju/cloudinit/sshinit"
    20  	"github.com/juju/juju/environs/cloudinit"
    21  	"github.com/juju/juju/environs/config"
    22  	"github.com/juju/juju/instance"
    23  	"github.com/juju/juju/network"
    24  	"github.com/juju/juju/state/multiwatcher"
    25  )
    26  
    27  const manualInstancePrefix = "manual:"
    28  
    29  var logger = loggo.GetLogger("juju.environs.manual")
    30  
    31  // ProvisioningClientAPI defines the methods that are needed for the manual
    32  // provisioning of machines.  An interface is used here to decouple the API
    33  // consumer from the actual API implementation type.
    34  type ProvisioningClientAPI interface {
    35  	AddMachines([]params.AddMachineParams) ([]params.AddMachinesResult, error)
    36  	ForceDestroyMachines(machines ...string) error
    37  	ProvisioningScript(params.ProvisioningScriptParams) (script string, err error)
    38  }
    39  
    40  type ProvisionMachineArgs struct {
    41  	// Host is the SSH host: [user@]host
    42  	Host string
    43  
    44  	// DataDir is the root directory for juju data.
    45  	// If left blank, the default location "/var/lib/juju" will be used.
    46  	DataDir string
    47  
    48  	// Client provides the API needed to provision the machines.
    49  	Client ProvisioningClientAPI
    50  
    51  	// Stdin is required to respond to sudo prompts,
    52  	// and must be a terminal (except in tests)
    53  	Stdin io.Reader
    54  
    55  	// Stdout is required to present sudo prompts to the user.
    56  	Stdout io.Writer
    57  
    58  	// Stderr is required to present machine provisioning progress to the user.
    59  	Stderr io.Writer
    60  
    61  	*params.UpdateBehavior
    62  }
    63  
    64  // ErrProvisioned is returned by ProvisionMachine if the target
    65  // machine has an existing machine agent.
    66  var ErrProvisioned = errors.New("machine is already provisioned")
    67  
    68  // ProvisionMachine provisions a machine agent to an existing host, via
    69  // an SSH connection to the specified host. The host may optionally be preceded
    70  // with a login username, as in [user@]host.
    71  //
    72  // On successful completion, this function will return the id of the state.Machine
    73  // that was entered into state.
    74  func ProvisionMachine(args ProvisionMachineArgs) (machineId string, err error) {
    75  	defer func() {
    76  		if machineId != "" && err != nil {
    77  			logger.Errorf("provisioning failed, removing machine %v: %v", machineId, err)
    78  			if cleanupErr := args.Client.ForceDestroyMachines(machineId); cleanupErr != nil {
    79  				logger.Warningf("error cleaning up machine: %s", cleanupErr)
    80  			}
    81  			machineId = ""
    82  		}
    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(args.Client, *machineParams)
   102  	if err != nil {
   103  		return "", err
   104  	}
   105  
   106  	provisioningScript, err := args.Client.ProvisioningScript(params.ProvisioningScriptParams{
   107  		MachineId: machineId,
   108  		Nonce:     machineParams.Nonce,
   109  		DisablePackageCommands: !args.EnableOSRefreshUpdate && !args.EnableOSUpgrade,
   110  	})
   111  
   112  	if err != nil {
   113  		logger.Errorf("cannot obtain provisioning script")
   114  		return "", err
   115  	}
   116  
   117  	// Finally, provision the machine agent.
   118  	err = runProvisionScript(provisioningScript, hostname, args.Stderr)
   119  	if err != nil {
   120  		return machineId, err
   121  	}
   122  
   123  	logger.Infof("Provisioned machine %v", machineId)
   124  	return machineId, nil
   125  }
   126  
   127  func splitUserHost(host string) (string, string) {
   128  	if at := strings.Index(host, "@"); at != -1 {
   129  		return host[:at], host[at+1:]
   130  	}
   131  	return "", host
   132  }
   133  
   134  func recordMachineInState(client ProvisioningClientAPI, machineParams params.AddMachineParams) (machineId string, err error) {
   135  	results, err := client.AddMachines([]params.AddMachineParams{machineParams})
   136  	if err != nil {
   137  		return "", err
   138  	}
   139  	// Currently, only one machine is added, but in future there may be several added in one call.
   140  	machineInfo := results[0]
   141  	if machineInfo.Error != nil {
   142  		return "", machineInfo.Error
   143  	}
   144  	return machineInfo.Machine, nil
   145  }
   146  
   147  // gatherMachineParams collects all the information we know about the machine
   148  // we are about to provision. It will SSH into that machine as the ubuntu user.
   149  // The hostname supplied should not include a username.
   150  // If we can, we will reverse lookup the hostname by its IP address, and use
   151  // the DNS resolved name, rather than the name that was supplied
   152  func gatherMachineParams(hostname string) (*params.AddMachineParams, error) {
   153  
   154  	// Generate a unique nonce for the machine.
   155  	uuid, err := utils.NewUUID()
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	var addrs []network.Address
   161  	if addr, err := HostAddress(hostname); err != nil {
   162  		logger.Warningf("failed to compute public address for %q: %v", hostname, err)
   163  	} else {
   164  		addrs = append(addrs, addr)
   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  	// Also, manually provisioned machines don't have the JobManageNetworking.
   188  	// This ensures that the networker is running in non-intrusive mode
   189  	// and never touches the network configuration files.
   190  	// No JobManageNetworking here due to manual provisioning.
   191  
   192  	instanceId := instance.Id(manualInstancePrefix + hostname)
   193  	nonce := fmt.Sprintf("%s:%s", instanceId, uuid.String())
   194  	machineParams := &params.AddMachineParams{
   195  		Series:                  series,
   196  		HardwareCharacteristics: hc,
   197  		InstanceId:              instanceId,
   198  		Nonce:                   nonce,
   199  		Addrs:                   addrs,
   200  		Jobs:                    []multiwatcher.MachineJob{multiwatcher.JobHostUnits},
   201  	}
   202  	return machineParams, nil
   203  }
   204  
   205  var provisionMachineAgent = func(host string, mcfg *cloudinit.MachineConfig, progressWriter io.Writer) error {
   206  	script, err := ProvisioningScript(mcfg)
   207  	if err != nil {
   208  		return err
   209  	}
   210  	return runProvisionScript(script, host, progressWriter)
   211  }
   212  
   213  // ProvisioningScript generates a bash script that can be
   214  // executed on a remote host to carry out the cloud-init
   215  // configuration.
   216  func ProvisioningScript(mcfg *cloudinit.MachineConfig) (string, error) {
   217  
   218  	cloudcfg := coreCloudinit.New()
   219  	cloudcfg.SetAptUpdate(mcfg.EnableOSRefreshUpdate)
   220  	cloudcfg.SetAptUpgrade(mcfg.EnableOSUpgrade)
   221  
   222  	udata, err := cloudinit.NewUserdataConfig(mcfg, cloudcfg)
   223  	if err != nil {
   224  		return "", errors.Annotate(err, "error generating cloud-config")
   225  	}
   226  	if err := udata.ConfigureJuju(); err != nil {
   227  		return "", errors.Annotate(err, "error generating cloud-config")
   228  	}
   229  
   230  	configScript, err := sshinit.ConfigureScript(cloudcfg)
   231  	if err != nil {
   232  		return "", errors.Annotate(err, "error converting cloud-config to script")
   233  	}
   234  
   235  	var buf bytes.Buffer
   236  	// Always remove the cloud-init-output.log file first, if it exists.
   237  	fmt.Fprintf(&buf, "rm -f %s\n", utils.ShQuote(mcfg.CloudInitOutputLog))
   238  	// If something goes wrong, dump cloud-init-output.log to stderr.
   239  	buf.WriteString(shell.DumpFileOnErrorScript(mcfg.CloudInitOutputLog))
   240  	buf.WriteString(configScript)
   241  	return buf.String(), nil
   242  }
   243  
   244  func runProvisionScript(script, host string, progressWriter io.Writer) error {
   245  	params := sshinit.ConfigureParams{
   246  		Host:           "ubuntu@" + host,
   247  		ProgressWriter: progressWriter,
   248  	}
   249  	return sshinit.RunConfigureScript(script, params)
   250  }