launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net"
    11  	"strings"
    12  
    13  	"github.com/loggo/loggo"
    14  
    15  	coreCloudinit "launchpad.net/juju-core/cloudinit"
    16  	"launchpad.net/juju-core/cloudinit/sshinit"
    17  	"launchpad.net/juju-core/environs/cloudinit"
    18  	"launchpad.net/juju-core/environs/config"
    19  	"launchpad.net/juju-core/instance"
    20  	"launchpad.net/juju-core/juju"
    21  	"launchpad.net/juju-core/state"
    22  	"launchpad.net/juju-core/state/api"
    23  	"launchpad.net/juju-core/state/api/params"
    24  	"launchpad.net/juju-core/state/statecmd"
    25  	"launchpad.net/juju-core/tools"
    26  	"launchpad.net/juju-core/utils"
    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  	// Used for fallback to 1.16 code
    75  	var stateConn *juju.Conn
    76  	defer func() {
    77  		if machineId != "" && err != nil {
    78  			logger.Errorf("provisioning failed, removing machine %v: %v", machineId, err)
    79  			// If we have stateConn, then we are in 1.16
    80  			// compatibility mode and we should issue
    81  			// DestroyMachines directly on the state, rather than
    82  			// via API (because DestroyMachine *also* didn't exist
    83  			// in 1.16, though it will be in 1.16.5).
    84  			// TODO: When this compatibility code is removed, we
    85  			// should remove the method in state as well (as long
    86  			// as destroy-machine also no longer needs it.)
    87  			var cleanupErr error
    88  			if stateConn != nil {
    89  				cleanupErr = statecmd.DestroyMachines1dot16(stateConn.State, machineId)
    90  			} else {
    91  				cleanupErr = client.DestroyMachines(machineId)
    92  			}
    93  			if cleanupErr != nil {
    94  				logger.Warningf("error cleaning up machine: %s", cleanupErr)
    95  			}
    96  			machineId = ""
    97  		}
    98  		if stateConn != nil {
    99  			stateConn.Close()
   100  			stateConn = nil
   101  		}
   102  		client.Close()
   103  	}()
   104  
   105  	// Create the "ubuntu" user and initialise passwordless sudo. We populate
   106  	// the ubuntu user's authorized_keys file with the public keys in the current
   107  	// user's ~/.ssh directory. The authenticationworker will later update the
   108  	// ubuntu user's authorized_keys.
   109  	user, hostname := splitUserHost(args.Host)
   110  	authorizedKeys, err := config.ReadAuthorizedKeys("")
   111  	if err := InitUbuntuUser(hostname, user, authorizedKeys, args.Stdin, args.Stdout); err != nil {
   112  		return "", err
   113  	}
   114  
   115  	machineParams, err := gatherMachineParams(hostname)
   116  	if err != nil {
   117  		return "", err
   118  	}
   119  
   120  	// Inform Juju that the machine exists.
   121  	machineId, err = recordMachineInState(client, *machineParams)
   122  	if params.IsCodeNotImplemented(err) {
   123  		logger.Infof("AddMachines not supported by the API server, " +
   124  			"falling back to 1.16 compatibility mode (direct DB access)")
   125  		stateConn, err = juju.NewConnFromName(args.EnvName)
   126  		if err == nil {
   127  			machineId, err = recordMachineInState1dot16(stateConn, *machineParams)
   128  		}
   129  	}
   130  	if err != nil {
   131  		return "", err
   132  	}
   133  
   134  	var provisioningScript string
   135  	if stateConn == nil {
   136  		provisioningScript, err = client.ProvisioningScript(params.ProvisioningScriptParams{
   137  			MachineId: machineId,
   138  			Nonce:     machineParams.Nonce,
   139  		})
   140  		if err != nil {
   141  			return "", err
   142  		}
   143  	} else {
   144  		mcfg, err := statecmd.MachineConfig(stateConn.State, machineId, machineParams.Nonce, args.DataDir)
   145  		if err == nil {
   146  			provisioningScript, err = generateProvisioningScript(mcfg)
   147  		}
   148  		if err != nil {
   149  			return "", err
   150  		}
   151  	}
   152  
   153  	// Finally, provision the machine agent.
   154  	err = runProvisionScript(provisioningScript, hostname, args.Stderr)
   155  	if err != nil {
   156  		return machineId, err
   157  	}
   158  
   159  	logger.Infof("Provisioned machine %v", machineId)
   160  	return machineId, nil
   161  }
   162  
   163  func splitUserHost(host string) (string, string) {
   164  	if at := strings.Index(host, "@"); at != -1 {
   165  		return host[:at], host[at+1:]
   166  	}
   167  	return "", host
   168  }
   169  
   170  func recordMachineInState(
   171  	client *api.Client, machineParams params.AddMachineParams) (machineId string, err error) {
   172  	results, err := client.AddMachines([]params.AddMachineParams{machineParams})
   173  	if err != nil {
   174  		return "", err
   175  	}
   176  	// Currently, only one machine is added, but in future there may be several added in one call.
   177  	machineInfo := results[0]
   178  	if machineInfo.Error != nil {
   179  		return "", machineInfo.Error
   180  	}
   181  	return machineInfo.Machine, nil
   182  }
   183  
   184  // convertToStateJobs takes a slice of params.MachineJob and makes them a slice of state.MachineJob
   185  func convertToStateJobs(jobs []params.MachineJob) ([]state.MachineJob, error) {
   186  	outJobs := make([]state.MachineJob, len(jobs))
   187  	var err error
   188  	for j, job := range jobs {
   189  		if outJobs[j], err = state.MachineJobFromParams(job); err != nil {
   190  			return nil, err
   191  		}
   192  	}
   193  	return outJobs, nil
   194  }
   195  
   196  func recordMachineInState1dot16(
   197  	stateConn *juju.Conn, machineParams params.AddMachineParams) (machineId string, err error) {
   198  	stateJobs, err := convertToStateJobs(machineParams.Jobs)
   199  	if err != nil {
   200  		return "", err
   201  	}
   202  	//if p.Series == "" {
   203  	//	p.Series = defaultSeries
   204  	//}
   205  	template := state.MachineTemplate{
   206  		Series:      machineParams.Series,
   207  		Constraints: machineParams.Constraints,
   208  		InstanceId:  machineParams.InstanceId,
   209  		Jobs:        stateJobs,
   210  		Nonce:       machineParams.Nonce,
   211  		HardwareCharacteristics: machineParams.HardwareCharacteristics,
   212  		Addresses:               machineParams.Addrs,
   213  	}
   214  	machine, err := stateConn.State.AddOneMachine(template)
   215  	if err != nil {
   216  		return "", err
   217  	}
   218  	return machine.Id(), nil
   219  }
   220  
   221  // gatherMachineParams collects all the information we know about the machine
   222  // we are about to provision. It will SSH into that machine as the ubuntu user.
   223  // The hostname supplied should not include a username.
   224  // If we can, we will reverse lookup the hostname by its IP address, and use
   225  // the DNS resolved name, rather than the name that was supplied
   226  func gatherMachineParams(hostname string) (*params.AddMachineParams, error) {
   227  
   228  	// Generate a unique nonce for the machine.
   229  	uuid, err := utils.NewUUID()
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  	// First, gather the parameters needed to inject the existing host into state.
   234  	if ip := net.ParseIP(hostname); ip != nil {
   235  		// Do a reverse-lookup on the IP. The IP may not have
   236  		// a DNS entry, so just log a warning if this fails.
   237  		names, err := net.LookupAddr(ip.String())
   238  		if err != nil {
   239  			logger.Infof("failed to resolve %v: %v", ip, err)
   240  		} else {
   241  			logger.Infof("resolved %v to %v", ip, names)
   242  			hostname = names[0]
   243  			// TODO: jam 2014-01-09 https://bugs.launchpad.net/bugs/1267387
   244  			// We change what 'hostname' we are using here (rather
   245  			// than an IP address we use the DNS name). I'm not
   246  			// sure why that is better, but if we are changing the
   247  			// host, we should probably be returning the hostname
   248  			// to the parent function.
   249  			// Also, we don't seem to try and compare if 'ip' is in
   250  			// the list of addrs returned from
   251  			// instance.HostAddresses in case you might get
   252  			// multiple and one of them is what you are supposed to
   253  			// be using.
   254  		}
   255  	}
   256  	addrs, err := HostAddresses(hostname)
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  	logger.Infof("addresses for %v: %v", hostname, addrs)
   261  
   262  	provisioned, err := checkProvisioned(hostname)
   263  	if err != nil {
   264  		err = fmt.Errorf("error checking if provisioned: %v", err)
   265  		return nil, err
   266  	}
   267  	if provisioned {
   268  		return nil, ErrProvisioned
   269  	}
   270  
   271  	hc, series, err := DetectSeriesAndHardwareCharacteristics(hostname)
   272  	if err != nil {
   273  		err = fmt.Errorf("error detecting hardware characteristics: %v", err)
   274  		return nil, err
   275  	}
   276  
   277  	// There will never be a corresponding "instance" that any provider
   278  	// knows about. This is fine, and works well with the provisioner
   279  	// task. The provisioner task will happily remove any and all dead
   280  	// machines from state, but will ignore the associated instance ID
   281  	// if it isn't one that the environment provider knows about.
   282  
   283  	instanceId := instance.Id(manualInstancePrefix + hostname)
   284  	nonce := fmt.Sprintf("%s:%s", instanceId, uuid.String())
   285  	machineParams := &params.AddMachineParams{
   286  		Series:                  series,
   287  		HardwareCharacteristics: hc,
   288  		InstanceId:              instanceId,
   289  		Nonce:                   nonce,
   290  		Addrs:                   addrs,
   291  		Jobs:                    []params.MachineJob{params.JobHostUnits},
   292  	}
   293  	return machineParams, nil
   294  }
   295  
   296  func provisionMachineAgent(host string, mcfg *cloudinit.MachineConfig, progressWriter io.Writer) error {
   297  	script, err := generateProvisioningScript(mcfg)
   298  	if err != nil {
   299  		return err
   300  	}
   301  	return runProvisionScript(script, host, progressWriter)
   302  }
   303  
   304  func generateProvisioningScript(mcfg *cloudinit.MachineConfig) (string, error) {
   305  	cloudcfg := coreCloudinit.New()
   306  	if err := cloudinit.ConfigureJuju(mcfg, cloudcfg); err != nil {
   307  		return "", err
   308  	}
   309  	// Explicitly disabling apt_upgrade so as not to trample
   310  	// the target machine's existing configuration.
   311  	cloudcfg.SetAptUpgrade(false)
   312  	return sshinit.ConfigureScript(cloudcfg)
   313  }
   314  
   315  func runProvisionScript(script, host string, progressWriter io.Writer) error {
   316  	params := sshinit.ConfigureParams{
   317  		Host:           "ubuntu@" + host,
   318  		ProgressWriter: progressWriter,
   319  	}
   320  	return sshinit.RunConfigureScript(script, params)
   321  }