github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/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  	"strings"
     9  
    10  	"github.com/juju/cmd"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/names"
    13  	"launchpad.net/gnuflag"
    14  
    15  	"github.com/juju/juju/api/machinemanager"
    16  	"github.com/juju/juju/apiserver/params"
    17  	"github.com/juju/juju/cmd/envcmd"
    18  	"github.com/juju/juju/cmd/juju/block"
    19  	"github.com/juju/juju/constraints"
    20  	"github.com/juju/juju/environs/config"
    21  	"github.com/juju/juju/environs/configstore"
    22  	"github.com/juju/juju/environs/manual"
    23  	"github.com/juju/juju/instance"
    24  	"github.com/juju/juju/provider"
    25  	"github.com/juju/juju/state/multiwatcher"
    26  	"github.com/juju/juju/storage"
    27  	"github.com/juju/juju/version"
    28  )
    29  
    30  // sshHostPrefix is the prefix for a machine to be "manually provisioned".
    31  const sshHostPrefix = "ssh:"
    32  
    33  var addMachineDoc = `
    34  
    35  Juju supports adding machines using provider-specific machine instances
    36  (EC2 instances, OpenStack servers, MAAS nodes, etc.); existing machines
    37  running a supported operating system (see "manual provisioning" below),
    38  and containers on machines. Machines are created in a clean state and
    39  ready to have units deployed.
    40  
    41  Without any parameters, add machine will allocate a new provider-specific
    42  machine (multiple, if "-n" is provided). When adding a new machine, you
    43  may specify constraints for the machine to be provisioned; the provider
    44  will interpret these constraints in order to decide what kind of machine
    45  to allocate.
    46  
    47  If a container type is specified (e.g. "lxc"), then add machine will
    48  allocate a container of that type on a new provider-specific machine. It is
    49  also possible to add containers to existing machines using the format
    50  <container type>:<machine number>. Constraints cannot be combined with
    51  deploying a container to an existing machine. The currently supported
    52  container types are: $CONTAINER_TYPES$.
    53  
    54  Manual provisioning is the process of installing Juju on an existing machine
    55  and bringing it under Juju's management; currently this requires that the
    56  machine be running Ubuntu, that it be accessible via SSH, and be running on
    57  the same network as the API server.
    58  
    59  It is possible to override or augment constraints by passing provider-specific
    60  "placement directives" as an argument; these give the provider additional
    61  information about how to allocate the machine. For example, one can direct the
    62  MAAS provider to acquire a particular node by specifying its hostname.
    63  For more information on placement directives, see "juju help placement".
    64  
    65  Examples:
    66     juju machine add                      (starts a new machine)
    67     juju machine add -n 2                 (starts 2 new machines)
    68     juju machine add lxc                  (starts a new machine with an lxc container)
    69     juju machine add lxc -n 2             (starts 2 new machines with an lxc container)
    70     juju machine add lxc:4                (starts a new lxc container on machine 4)
    71     juju machine add --constraints mem=8G (starts a machine with at least 8GB RAM)
    72     juju machine add ssh:user@10.10.0.3   (manually provisions a machine with ssh)
    73     juju machine add zone=us-east-1a      (start a machine in zone us-east-1a on AWS)
    74     juju machine add maas2.name           (acquire machine maas2.name on MAAS)
    75  
    76  See Also:
    77     juju help constraints
    78     juju help placement
    79  `
    80  
    81  func init() {
    82  	containerTypes := make([]string, len(instance.ContainerTypes))
    83  	for i, t := range instance.ContainerTypes {
    84  		containerTypes[i] = string(t)
    85  	}
    86  	addMachineDoc = strings.Replace(
    87  		addMachineDoc,
    88  		"$CONTAINER_TYPES$",
    89  		strings.Join(containerTypes, ", "),
    90  		-1,
    91  	)
    92  }
    93  
    94  // AddCommand starts a new machine and registers it in the environment.
    95  type AddCommand struct {
    96  	envcmd.EnvCommandBase
    97  	api               AddMachineAPI
    98  	machineManagerAPI MachineManagerAPI
    99  	// If specified, use this series, else use the environment default-series
   100  	Series string
   101  	// If specified, these constraints are merged with those already in the environment.
   102  	Constraints constraints.Value
   103  	// Placement is passed verbatim to the API, to be parsed and evaluated server-side.
   104  	Placement *instance.Placement
   105  	// NumMachines is the number of machines to add.
   106  	NumMachines int
   107  	// Disks describes disks that are to be attached to the machine.
   108  	Disks []storage.Constraints
   109  }
   110  
   111  func (c *AddCommand) Info() *cmd.Info {
   112  	return &cmd.Info{
   113  		Name:    "add",
   114  		Args:    "[<container>:machine | <container> | ssh:[user@]host | placement]",
   115  		Purpose: "start a new, empty machine and optionally a container, or add a container to a machine",
   116  		Doc:     addMachineDoc,
   117  	}
   118  }
   119  
   120  func (c *AddCommand) SetFlags(f *gnuflag.FlagSet) {
   121  	f.StringVar(&c.Series, "series", "", "the charm series")
   122  	f.IntVar(&c.NumMachines, "n", 1, "The number of machines to add")
   123  	f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "additional machine constraints")
   124  	f.Var(disksFlag{&c.Disks}, "disks", "constraints for disks to attach to the machine")
   125  }
   126  
   127  func (c *AddCommand) Init(args []string) error {
   128  	if c.Constraints.Container != nil {
   129  		return fmt.Errorf("container constraint %q not allowed when adding a machine", *c.Constraints.Container)
   130  	}
   131  	placement, err := cmd.ZeroOrOneArgs(args)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	c.Placement, err = instance.ParsePlacement(placement)
   136  	if err == instance.ErrPlacementScopeMissing {
   137  		placement = "env-uuid" + ":" + placement
   138  		c.Placement, err = instance.ParsePlacement(placement)
   139  	}
   140  	if err != nil {
   141  		return err
   142  	}
   143  	if c.NumMachines > 1 && c.Placement != nil && c.Placement.Directive != "" {
   144  		return fmt.Errorf("cannot use -n when specifying a placement directive")
   145  	}
   146  	return nil
   147  }
   148  
   149  type AddMachineAPI interface {
   150  	AddMachines([]params.AddMachineParams) ([]params.AddMachinesResult, error)
   151  	AddMachines1dot18([]params.AddMachineParams) ([]params.AddMachinesResult, error)
   152  	Close() error
   153  	ForceDestroyMachines(machines ...string) error
   154  	EnvironmentGet() (map[string]interface{}, error)
   155  	EnvironmentUUID() string
   156  	ProvisioningScript(params.ProvisioningScriptParams) (script string, err error)
   157  }
   158  
   159  type MachineManagerAPI interface {
   160  	AddMachines([]params.AddMachineParams) ([]params.AddMachinesResult, error)
   161  	BestAPIVersion() int
   162  	Close() error
   163  }
   164  
   165  var manualProvisioner = manual.ProvisionMachine
   166  
   167  func (c *AddCommand) getClientAPI() (AddMachineAPI, error) {
   168  	if c.api != nil {
   169  		return c.api, nil
   170  	}
   171  	return c.NewAPIClient()
   172  }
   173  
   174  func (c *AddCommand) NewMachineManagerClient() (*machinemanager.Client, error) {
   175  	root, err := c.NewAPIRoot()
   176  	if err != nil {
   177  		return nil, errors.Trace(err)
   178  	}
   179  	return machinemanager.NewClient(root), nil
   180  }
   181  
   182  func (c *AddCommand) getMachineManagerAPI() (MachineManagerAPI, error) {
   183  	if c.machineManagerAPI != nil {
   184  		return c.machineManagerAPI, nil
   185  	}
   186  	return c.NewMachineManagerClient()
   187  }
   188  
   189  func (c *AddCommand) Run(ctx *cmd.Context) error {
   190  	client, err := c.getClientAPI()
   191  	if err != nil {
   192  		return errors.Trace(err)
   193  	}
   194  	defer client.Close()
   195  
   196  	var machineManager MachineManagerAPI
   197  	if len(c.Disks) > 0 {
   198  		machineManager, err = c.getMachineManagerAPI()
   199  		if err != nil {
   200  			return errors.Trace(err)
   201  		}
   202  		defer machineManager.Close()
   203  		if machineManager.BestAPIVersion() < 1 {
   204  			return errors.New("cannot add machines with disks: not supported by the API server")
   205  		}
   206  	}
   207  
   208  	logger.Infof("load config")
   209  	var config *config.Config
   210  	if defaultStore, err := configstore.Default(); err != nil {
   211  		return err
   212  	} else if config, err = c.Config(defaultStore, client); err != nil {
   213  		return err
   214  	}
   215  
   216  	if c.Placement != nil && c.Placement.Scope == "ssh" {
   217  		logger.Infof("manual provisioning")
   218  		args := manual.ProvisionMachineArgs{
   219  			Host:   c.Placement.Directive,
   220  			Client: client,
   221  			Stdin:  ctx.Stdin,
   222  			Stdout: ctx.Stdout,
   223  			Stderr: ctx.Stderr,
   224  			UpdateBehavior: &params.UpdateBehavior{
   225  				config.EnableOSRefreshUpdate(),
   226  				config.EnableOSUpgrade(),
   227  			},
   228  		}
   229  		machineId, err := manualProvisioner(args)
   230  		if err == nil {
   231  			ctx.Infof("created machine %v", machineId)
   232  		}
   233  		return err
   234  	}
   235  
   236  	logger.Infof("environment provisioning")
   237  	if c.Placement != nil && c.Placement.Scope == "env-uuid" {
   238  		c.Placement.Scope = client.EnvironmentUUID()
   239  	}
   240  
   241  	if c.Placement != nil && c.Placement.Scope == instance.MachineScope {
   242  		// It does not make sense to add-machine <id>.
   243  		return fmt.Errorf("machine-id cannot be specified when adding machines")
   244  	}
   245  
   246  	jobs := []multiwatcher.MachineJob{multiwatcher.JobHostUnits}
   247  
   248  	envVersion, err := envcmd.GetEnvironmentVersion(client)
   249  	if err != nil {
   250  		return err
   251  	}
   252  
   253  	// Servers before 1.21-alpha2 don't have the networker so don't
   254  	// try to use JobManageNetworking with them.
   255  	//
   256  	// In case of MAAS and Joyent JobManageNetworking is not added
   257  	// to ensure the non-intrusive start of a networker like above
   258  	// for the manual provisioning. See this related joyent bug
   259  	// http://pad.lv/1401423
   260  	if envVersion.Compare(version.MustParse("1.21-alpha2")) >= 0 &&
   261  		config.Type() != provider.MAAS &&
   262  		config.Type() != provider.Joyent {
   263  		jobs = append(jobs, multiwatcher.JobManageNetworking)
   264  	}
   265  
   266  	machineParams := params.AddMachineParams{
   267  		Placement:   c.Placement,
   268  		Series:      c.Series,
   269  		Constraints: c.Constraints,
   270  		Jobs:        jobs,
   271  		Disks:       c.Disks,
   272  	}
   273  	machines := make([]params.AddMachineParams, c.NumMachines)
   274  	for i := 0; i < c.NumMachines; i++ {
   275  		machines[i] = machineParams
   276  	}
   277  
   278  	var results []params.AddMachinesResult
   279  	// If storage is specified, we attempt to use a new API on the service facade.
   280  	if len(c.Disks) > 0 {
   281  		results, err = machineManager.AddMachines(machines)
   282  	} else {
   283  		results, err = client.AddMachines(machines)
   284  		if params.IsCodeNotImplemented(err) {
   285  			if c.Placement != nil {
   286  				containerType, parseErr := instance.ParseContainerType(c.Placement.Scope)
   287  				if parseErr != nil {
   288  					// The user specified a non-container placement directive:
   289  					// return original API not implemented error.
   290  					return err
   291  				}
   292  				machineParams.ContainerType = containerType
   293  				machineParams.ParentId = c.Placement.Directive
   294  				machineParams.Placement = nil
   295  			}
   296  			logger.Infof(
   297  				"AddMachinesWithPlacement not supported by the API server, " +
   298  					"falling back to 1.18 compatibility mode",
   299  			)
   300  			results, err = client.AddMachines1dot18([]params.AddMachineParams{machineParams})
   301  		}
   302  	}
   303  	if params.IsCodeOperationBlocked(err) {
   304  		return block.ProcessBlockedError(err, block.BlockChange)
   305  	}
   306  	if err != nil {
   307  		return errors.Trace(err)
   308  	}
   309  
   310  	errs := []error{}
   311  	for _, machineInfo := range results {
   312  		if machineInfo.Error != nil {
   313  			errs = append(errs, machineInfo.Error)
   314  			continue
   315  		}
   316  		machineId := machineInfo.Machine
   317  
   318  		if names.IsContainerMachine(machineId) {
   319  			ctx.Infof("created container %v", machineId)
   320  		} else {
   321  			ctx.Infof("created machine %v", machineId)
   322  		}
   323  	}
   324  	if len(errs) == 1 {
   325  		fmt.Fprintf(ctx.Stderr, "failed to create 1 machine\n")
   326  		return errs[0]
   327  	}
   328  	if len(errs) > 1 {
   329  		fmt.Fprintf(ctx.Stderr, "failed to create %d machines\n", len(errs))
   330  		returnErr := []string{}
   331  		for _, e := range errs {
   332  			returnErr = append(returnErr, e.Error())
   333  		}
   334  		return errors.New(strings.Join(returnErr, ", "))
   335  	}
   336  	return nil
   337  }