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: ¶ms.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 }