github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/machine/add.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package machine
     5  
     6  import (
     7  	"fmt"
     8  	"path/filepath"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/juju/cmd"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/gnuflag"
    15  	"github.com/juju/utils/winrm"
    16  	"gopkg.in/juju/names.v2"
    17  
    18  	"github.com/juju/juju/api/machinemanager"
    19  	"github.com/juju/juju/api/modelconfig"
    20  	"github.com/juju/juju/apiserver/params"
    21  	jujucmd "github.com/juju/juju/cmd"
    22  	"github.com/juju/juju/cmd/juju/block"
    23  	"github.com/juju/juju/cmd/juju/common"
    24  	"github.com/juju/juju/cmd/modelcmd"
    25  	"github.com/juju/juju/core/constraints"
    26  	"github.com/juju/juju/core/instance"
    27  	"github.com/juju/juju/environs/config"
    28  	"github.com/juju/juju/environs/manual"
    29  	"github.com/juju/juju/environs/manual/sshprovisioner"
    30  	"github.com/juju/juju/environs/manual/winrmprovisioner"
    31  	"github.com/juju/juju/juju/osenv"
    32  	"github.com/juju/juju/state/multiwatcher"
    33  	"github.com/juju/juju/storage"
    34  )
    35  
    36  var addMachineDoc = `
    37  Juju supports adding machines using provider-specific machine instances
    38  (EC2 instances, OpenStack servers, MAAS nodes, etc.); existing machines
    39  running a supported operating system (see "manual provisioning" below),
    40  and containers on machines. Machines are created in a clean state and
    41  ready to have units deployed.
    42  
    43  Without any parameters, add machine will allocate a new provider-specific
    44  machine (multiple, if "-n" is provided). When adding a new machine, you
    45  may specify constraints for the machine to be provisioned; the provider
    46  will interpret these constraints in order to decide what kind of machine
    47  to allocate.
    48  
    49  If a container type is specified (e.g. "lxd"), then add machine will
    50  allocate a container of that type on a new provider-specific machine. It is
    51  also possible to add containers to existing machines using the format
    52  <container type>:<machine number>. Constraints cannot be combined with
    53  deploying a container to an existing machine. The currently supported
    54  container types are: $CONTAINER_TYPES$.
    55  
    56  Manual provisioning is the process of installing Juju on an existing machine
    57  and bringing it under Juju's management; currently this requires that the
    58  machine be running Ubuntu, that it be accessible via SSH, and be running on
    59  the same network as the API server.
    60  
    61  It is possible to override or augment constraints by passing provider-specific
    62  "placement directives" as an argument; these give the provider additional
    63  information about how to allocate the machine. For example, one can direct the
    64  MAAS provider to acquire a particular node by specifying its hostname.
    65  
    66  Examples:
    67     juju add-machine                      (starts a new machine)
    68     juju add-machine -n 2                 (starts 2 new machines)
    69     juju add-machine lxd                  (starts a new machine with an lxd container)
    70     juju add-machine lxd -n 2             (starts 2 new machines with an lxd container)
    71     juju add-machine lxd:4                (starts a new lxd container on machine 4)
    72     juju add-machine --constraints mem=8G (starts a machine with at least 8GB RAM)
    73     juju add-machine ssh:user@10.10.0.3   (manually provisions machine with ssh)
    74     juju add-machine winrm:user@10.10.0.3 (manually provisions machine with winrm)
    75     juju add-machine zone=us-east-1a      (start a machine in zone us-east-1a on AWS)
    76     juju add-machine maas2.name           (acquire machine maas2.name on MAAS)
    77  
    78  See also:
    79      remove-machine
    80  `
    81  
    82  func init() {
    83  	containerTypes := make([]string, len(instance.ContainerTypes))
    84  	for i, t := range instance.ContainerTypes {
    85  		containerTypes[i] = string(t)
    86  	}
    87  	addMachineDoc = strings.Replace(
    88  		addMachineDoc,
    89  		"$CONTAINER_TYPES$",
    90  		strings.Join(containerTypes, ", "),
    91  		-1,
    92  	)
    93  }
    94  
    95  // NewAddCommand returns a command that adds a machine to a model.
    96  func NewAddCommand() cmd.Command {
    97  	return modelcmd.Wrap(&addCommand{})
    98  }
    99  
   100  // addCommand starts a new machine and registers it in the model.
   101  type addCommand struct {
   102  	baseMachinesCommand
   103  	api               AddMachineAPI
   104  	modelConfigAPI    ModelConfigAPI
   105  	machineManagerAPI MachineManagerAPI
   106  	// If specified, use this series, else use the model default-series
   107  	Series string
   108  	// If specified, these constraints are merged with those already in the model.
   109  	Constraints constraints.Value
   110  	// If specified, these constraints are merged with those already in the model.
   111  	ConstraintsStr string
   112  	// Placement is passed verbatim to the API, to be parsed and evaluated server-side.
   113  	Placement *instance.Placement
   114  	// NumMachines is the number of machines to add.
   115  	NumMachines int
   116  	// Disks describes disks that are to be attached to the machine.
   117  	Disks []storage.Constraints
   118  }
   119  
   120  func (c *addCommand) Info() *cmd.Info {
   121  	return jujucmd.Info(&cmd.Info{
   122  		Name:    "add-machine",
   123  		Args:    "[<container>:machine | <container> | ssh:[user@]host | winrm:[user@]host | placement]",
   124  		Purpose: "Start a new, empty machine and optionally a container, or add a container to a machine.",
   125  		Doc:     addMachineDoc,
   126  	})
   127  }
   128  
   129  func (c *addCommand) SetFlags(f *gnuflag.FlagSet) {
   130  	c.ModelCommandBase.SetFlags(f)
   131  	f.StringVar(&c.Series, "series", "", "The charm series")
   132  	f.IntVar(&c.NumMachines, "n", 1, "The number of machines to add")
   133  	f.StringVar(&c.ConstraintsStr, "constraints", "", "Additional machine constraints")
   134  	f.Var(disksFlag{&c.Disks}, "disks", "Constraints for disks to attach to the machine")
   135  }
   136  
   137  func (c *addCommand) Init(args []string) error {
   138  	if c.Constraints.Container != nil {
   139  		return errors.Errorf("container constraint %q not allowed when adding a machine", *c.Constraints.Container)
   140  	}
   141  	placement, err := cmd.ZeroOrOneArgs(args)
   142  	if err != nil {
   143  		return err
   144  	}
   145  	c.Placement, err = instance.ParsePlacement(placement)
   146  	if err == instance.ErrPlacementScopeMissing {
   147  		placement = "model-uuid" + ":" + placement
   148  		c.Placement, err = instance.ParsePlacement(placement)
   149  	}
   150  	if err != nil {
   151  		return err
   152  	}
   153  	if c.NumMachines > 1 && c.Placement != nil && c.Placement.Directive != "" {
   154  		return errors.New("cannot use -n when specifying a placement directive")
   155  	}
   156  	return nil
   157  }
   158  
   159  type AddMachineAPI interface {
   160  	AddMachines([]params.AddMachineParams) ([]params.AddMachinesResult, error)
   161  	Close() error
   162  	ForceDestroyMachines(machines ...string) error
   163  	DestroyMachinesWithParams(force, keep bool, machines ...string) error
   164  	ModelUUID() (string, bool)
   165  	ProvisioningScript(params.ProvisioningScriptParams) (script string, err error)
   166  }
   167  
   168  type ModelConfigAPI interface {
   169  	ModelGet() (map[string]interface{}, error)
   170  	Close() error
   171  }
   172  
   173  type MachineManagerAPI interface {
   174  	AddMachines([]params.AddMachineParams) ([]params.AddMachinesResult, error)
   175  	BestAPIVersion() int
   176  	Close() error
   177  }
   178  
   179  // splitUserHost given a host string of example user@192.168.122.122
   180  // it will return user and 192.168.122.122
   181  func splitUserHost(host string) (string, string) {
   182  	if at := strings.Index(host, "@"); at != -1 {
   183  		return host[:at], host[at+1:]
   184  	}
   185  	return "", host
   186  }
   187  
   188  func (c *addCommand) getClientAPI() (AddMachineAPI, error) {
   189  	if c.api != nil {
   190  		return c.api, nil
   191  	}
   192  	return c.NewAPIClient()
   193  }
   194  
   195  func (c *addCommand) getModelConfigAPI() (ModelConfigAPI, error) {
   196  	if c.modelConfigAPI != nil {
   197  		return c.modelConfigAPI, nil
   198  	}
   199  	api, err := c.NewAPIRoot()
   200  	if err != nil {
   201  		return nil, errors.Annotate(err, "opening API connection")
   202  	}
   203  	return modelconfig.NewClient(api), nil
   204  
   205  }
   206  
   207  func (c *addCommand) NewMachineManagerClient() (*machinemanager.Client, error) {
   208  	root, err := c.NewAPIRoot()
   209  	if err != nil {
   210  		return nil, errors.Trace(err)
   211  	}
   212  	return machinemanager.NewClient(root), nil
   213  }
   214  
   215  func (c *addCommand) getMachineManagerAPI() (MachineManagerAPI, error) {
   216  	if c.machineManagerAPI != nil {
   217  		return c.machineManagerAPI, nil
   218  	}
   219  	return c.NewMachineManagerClient()
   220  }
   221  
   222  func (c *addCommand) Run(ctx *cmd.Context) error {
   223  	var err error
   224  	c.Constraints, err = common.ParseConstraints(ctx, c.ConstraintsStr)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	client, err := c.getClientAPI()
   229  	if err != nil {
   230  		return errors.Trace(err)
   231  	}
   232  	defer client.Close()
   233  
   234  	var machineManager MachineManagerAPI
   235  	if len(c.Disks) > 0 {
   236  		machineManager, err = c.getMachineManagerAPI()
   237  		if err != nil {
   238  			return errors.Trace(err)
   239  		}
   240  		defer machineManager.Close()
   241  		if machineManager.BestAPIVersion() < 1 {
   242  			return errors.New("cannot add machines with disks: not supported by the API server")
   243  		}
   244  	}
   245  
   246  	logger.Infof("load config")
   247  	modelConfigClient, err := c.getModelConfigAPI()
   248  	if err != nil {
   249  		return errors.Trace(err)
   250  	}
   251  	defer modelConfigClient.Close()
   252  	configAttrs, err := modelConfigClient.ModelGet()
   253  	if err != nil {
   254  		if params.IsCodeUnauthorized(err) {
   255  			common.PermissionsMessage(ctx.Stderr, "add a machine to this model")
   256  		}
   257  		return errors.Trace(err)
   258  	}
   259  	config, err := config.New(config.NoDefaults, configAttrs)
   260  	if err != nil {
   261  		return errors.Trace(err)
   262  	}
   263  
   264  	if c.Placement != nil {
   265  		err := c.tryManualProvision(client, config, ctx)
   266  		if err != errNonManualScope {
   267  			return err
   268  		}
   269  	}
   270  
   271  	logger.Infof("model provisioning")
   272  	if c.Placement != nil && c.Placement.Scope == "model-uuid" {
   273  		uuid, ok := client.ModelUUID()
   274  		if !ok {
   275  			return errors.New("API connection is controller-only (should never happen)")
   276  		}
   277  		c.Placement.Scope = uuid
   278  	}
   279  
   280  	if c.Placement != nil && c.Placement.Scope == instance.MachineScope {
   281  		// It does not make sense to add-machine <id>.
   282  		return errors.Errorf("machine-id cannot be specified when adding machines")
   283  	}
   284  
   285  	jobs := []multiwatcher.MachineJob{multiwatcher.JobHostUnits}
   286  
   287  	machineParams := params.AddMachineParams{
   288  		Placement:   c.Placement,
   289  		Series:      c.Series,
   290  		Constraints: c.Constraints,
   291  		Jobs:        jobs,
   292  		Disks:       c.Disks,
   293  	}
   294  	machines := make([]params.AddMachineParams, c.NumMachines)
   295  	for i := 0; i < c.NumMachines; i++ {
   296  		machines[i] = machineParams
   297  	}
   298  
   299  	var results []params.AddMachinesResult
   300  	// If storage is specified, we attempt to use a new API on the application facade.
   301  	if len(c.Disks) > 0 {
   302  		results, err = machineManager.AddMachines(machines)
   303  	} else {
   304  		results, err = client.AddMachines(machines)
   305  	}
   306  	if params.IsCodeOperationBlocked(err) {
   307  		return block.ProcessBlockedError(err, block.BlockChange)
   308  	}
   309  	if err != nil {
   310  		return errors.Trace(err)
   311  	}
   312  
   313  	errs := []error{}
   314  	for _, machineInfo := range results {
   315  		if machineInfo.Error != nil {
   316  			errs = append(errs, machineInfo.Error)
   317  			continue
   318  		}
   319  		machineId := machineInfo.Machine
   320  
   321  		if names.IsContainerMachine(machineId) {
   322  			ctx.Infof("created container %v", machineId)
   323  		} else {
   324  			ctx.Infof("created machine %v", machineId)
   325  		}
   326  	}
   327  	if len(errs) == 1 {
   328  		fmt.Fprint(ctx.Stderr, "failed to create 1 machine\n")
   329  		return errs[0]
   330  	}
   331  	if len(errs) > 1 {
   332  		fmt.Fprintf(ctx.Stderr, "failed to create %d machines\n", len(errs))
   333  		returnErr := []string{}
   334  		for _, e := range errs {
   335  			returnErr = append(returnErr, e.Error())
   336  		}
   337  		return errors.New(strings.Join(returnErr, ", "))
   338  	}
   339  	return nil
   340  }
   341  
   342  var (
   343  	sshProvisioner    = sshprovisioner.ProvisionMachine
   344  	winrmProvisioner  = winrmprovisioner.ProvisionMachine
   345  	errNonManualScope = errors.New("non-manual scope")
   346  	sshScope          = "ssh"
   347  	winrmScope        = "winrm"
   348  )
   349  
   350  func (c *addCommand) tryManualProvision(client AddMachineAPI, config *config.Config, ctx *cmd.Context) error {
   351  
   352  	var provisionMachine manual.ProvisionMachineFunc
   353  	switch c.Placement.Scope {
   354  	case sshScope:
   355  		provisionMachine = sshProvisioner
   356  	case winrmScope:
   357  		provisionMachine = c.provisionWinRM
   358  	default:
   359  		return errNonManualScope
   360  	}
   361  
   362  	authKeys, err := common.ReadAuthorizedKeys(ctx, "")
   363  	if err != nil {
   364  		return errors.Annotatef(err, "cannot reading authorized-keys")
   365  	}
   366  
   367  	user, host := splitUserHost(c.Placement.Directive)
   368  	args := manual.ProvisionMachineArgs{
   369  		Host:           host,
   370  		User:           user,
   371  		Client:         client,
   372  		Stdin:          ctx.Stdin,
   373  		Stdout:         ctx.Stdout,
   374  		Stderr:         ctx.Stderr,
   375  		AuthorizedKeys: authKeys,
   376  		UpdateBehavior: &params.UpdateBehavior{
   377  			EnableOSRefreshUpdate: config.EnableOSRefreshUpdate(),
   378  			EnableOSUpgrade:       config.EnableOSUpgrade(),
   379  		},
   380  	}
   381  
   382  	machineId, err := provisionMachine(args)
   383  	if err != nil {
   384  		return errors.Trace(err)
   385  	}
   386  	ctx.Infof("created machine %v", machineId)
   387  	return nil
   388  }
   389  
   390  func (c *addCommand) provisionWinRM(args manual.ProvisionMachineArgs) (string, error) {
   391  	base := osenv.JujuXDGDataHomePath("x509")
   392  	keyPath := filepath.Join(base, "winrmkey.pem")
   393  	certPath := filepath.Join(base, "winrmcert.crt")
   394  	cert := winrm.NewX509()
   395  	if err := cert.LoadClientCert(keyPath, certPath); err != nil {
   396  		return "", errors.Annotatef(err, "connot load/create x509 client certs for winrm connection")
   397  	}
   398  	if err := cert.LoadCACert(filepath.Join(base, "winrmcacert.crt")); err != nil {
   399  		logger.Infof("cannot not find any CA cert to load")
   400  	}
   401  
   402  	cfg := winrm.ClientConfig{
   403  		User:    args.User,
   404  		Host:    args.Host,
   405  		Key:     cert.ClientKey(),
   406  		Cert:    cert.ClientCert(),
   407  		Timeout: 25 * time.Second,
   408  		Secure:  true,
   409  	}
   410  
   411  	caCert := cert.CACert()
   412  	if caCert == nil {
   413  		logger.Infof("Skipping winrm CA validation")
   414  		cfg.Insecure = true
   415  	} else {
   416  		cfg.CACert = caCert
   417  	}
   418  
   419  	client, err := winrm.NewClient(cfg)
   420  	if err != nil {
   421  		return "", errors.Annotatef(err, "cannot create WinRM client connection")
   422  	}
   423  	args.WinRM = manual.WinRMArgs{
   424  		Keys:   cert,
   425  		Client: client,
   426  	}
   427  	return winrmProvisioner(args)
   428  }