github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/cmd/juju/deploy.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/juju/cmd"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/names"
    14  	"github.com/juju/utils/featureflag"
    15  	"gopkg.in/juju/charm.v4"
    16  	"launchpad.net/gnuflag"
    17  
    18  	"github.com/juju/juju/api"
    19  	"github.com/juju/juju/apiserver/params"
    20  	"github.com/juju/juju/cmd/envcmd"
    21  	"github.com/juju/juju/cmd/juju/block"
    22  	"github.com/juju/juju/constraints"
    23  	"github.com/juju/juju/environs/config"
    24  	"github.com/juju/juju/juju/osenv"
    25  	"github.com/juju/juju/storage"
    26  )
    27  
    28  type DeployCommand struct {
    29  	envcmd.EnvCommandBase
    30  	UnitCommandBase
    31  	CharmName    string
    32  	ServiceName  string
    33  	Config       cmd.FileVar
    34  	Constraints  constraints.Value
    35  	Networks     string
    36  	BumpRevision bool   // Remove this once the 1.16 support is dropped.
    37  	RepoPath     string // defaults to JUJU_REPOSITORY
    38  
    39  	// TODO(axw) move this to UnitCommandBase once we support --storage
    40  	// on add-unit too.
    41  	//
    42  	// Storage is a map of storage constraints, keyed on the storage name
    43  	// defined in charm storage metadata.
    44  	Storage map[string]storage.Constraints
    45  }
    46  
    47  const deployDoc = `
    48  <charm name> can be a charm URL, or an unambiguously condensed form of it;
    49  assuming a current series of "precise", the following forms will be accepted:
    50  
    51  For cs:precise/mysql
    52    mysql
    53    precise/mysql
    54  
    55  For cs:~user/precise/mysql
    56    cs:~user/mysql
    57  
    58  The current series is determined first by the default-series environment
    59  setting, followed by the preferred series for the charm in the charm store.
    60  
    61  In these cases, a versioned charm URL will be expanded as expected (for example,
    62  mysql-33 becomes cs:precise/mysql-33).
    63  
    64  However, for local charms, when the default-series is not specified in the
    65  environment, one must specify the series. For example:
    66    local:precise/mysql
    67  
    68  <service name>, if omitted, will be derived from <charm name>.
    69  
    70  Constraints can be specified when using deploy by specifying the --constraints
    71  flag.  When used with deploy, service-specific constraints are set so that later
    72  machines provisioned with add-unit will use the same constraints (unless changed
    73  by set-constraints).
    74  
    75  Charms can be deployed to a specific machine using the --to argument.
    76  If the destination is an LXC container the default is to use lxc-clone
    77  to create the container where possible. For Ubuntu deployments, lxc-clone
    78  is supported for the trusty OS series and later. A 'template' container is
    79  created with the name
    80    juju-<series>-template
    81  where <series> is the OS series, for example 'juju-trusty-template'.
    82  
    83  You can override the use of clone by changing the provider configuration:
    84    lxc-clone: false
    85  
    86  If you have the main container directory mounted on a btrfs partition,
    87  then the clone will be using btrfs snapshots to create the containers.
    88  This means that clones use up much less disk space.  If you do not have btrfs,
    89  lxc will attempt to use aufs (an overlay type filesystem). You can
    90  explicitly ask Juju to create full containers and not overlays by specifying
    91  the following in the provider configuration:
    92    lxc-clone-aufs: false
    93  
    94  Examples:
    95     juju deploy mysql --to 23       (deploy to machine 23)
    96     juju deploy mysql --to 24/lxc/3 (deploy to lxc container 3 on host machine 24)
    97     juju deploy mysql --to lxc:25   (deploy to a new lxc container on host machine 25)
    98  
    99     juju deploy mysql -n 5 --constraints mem=8G
   100     (deploy 5 instances of mysql with at least 8 GB of RAM each)
   101  
   102     juju deploy mysql --networks=storage,mynet --constraints networks=^logging,db
   103     (deploy mysql on machines with "storage", "mynet" and "db" networks,
   104      but not on machines with "logging" network, also configure "storage" and
   105      "mynet" networks)
   106  
   107  Like constraints, service-specific network requirements can be
   108  specified with the --networks argument, which takes a comma-delimited
   109  list of juju-specific network names. Networks can also be specified with
   110  constraints, but they only define what machine to pick, not what networks
   111  to configure on it. The --networks argument instructs juju to add all the
   112  networks specified with it to all new machines deployed to host units of
   113  the service. Not supported on all providers.
   114  
   115  See Also:
   116     juju help constraints
   117     juju help set-constraints
   118     juju help get-constraints
   119  `
   120  
   121  func (c *DeployCommand) Info() *cmd.Info {
   122  	return &cmd.Info{
   123  		Name:    "deploy",
   124  		Args:    "<charm name> [<service name>]",
   125  		Purpose: "deploy a new service",
   126  		Doc:     deployDoc,
   127  	}
   128  }
   129  
   130  func (c *DeployCommand) SetFlags(f *gnuflag.FlagSet) {
   131  	c.UnitCommandBase.SetFlags(f)
   132  	f.IntVar(&c.NumUnits, "n", 1, "number of service units to deploy for principal charms")
   133  	f.BoolVar(&c.BumpRevision, "u", false, "increment local charm directory revision (DEPRECATED)")
   134  	f.BoolVar(&c.BumpRevision, "upgrade", false, "")
   135  	f.Var(&c.Config, "config", "path to yaml-formatted service config")
   136  	f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set service constraints")
   137  	f.StringVar(&c.Networks, "networks", "", "bind the service to specific networks")
   138  	f.StringVar(&c.RepoPath, "repository", os.Getenv(osenv.JujuRepositoryEnvKey), "local charm repository")
   139  	if featureflag.Enabled(storage.FeatureFlag) {
   140  		// NOTE: if/when the feature flag is removed, bump the client
   141  		// facade and check that the ServiceDeployWithNetworks facade
   142  		// version supports storage, and error if it doesn't.
   143  		f.Var(storageFlag{&c.Storage}, "storage", "charm storage constraints")
   144  	}
   145  }
   146  
   147  func (c *DeployCommand) Init(args []string) error {
   148  	switch len(args) {
   149  	case 2:
   150  		if !names.IsValidService(args[1]) {
   151  			return fmt.Errorf("invalid service name %q", args[1])
   152  		}
   153  		c.ServiceName = args[1]
   154  		fallthrough
   155  	case 1:
   156  		if _, err := charm.InferURL(args[0], "fake"); err != nil {
   157  			return fmt.Errorf("invalid charm name %q", args[0])
   158  		}
   159  		c.CharmName = args[0]
   160  	case 0:
   161  		return errors.New("no charm specified")
   162  	default:
   163  		return cmd.CheckEmpty(args[2:])
   164  	}
   165  	return c.UnitCommandBase.Init(args)
   166  }
   167  
   168  func (c *DeployCommand) Run(ctx *cmd.Context) error {
   169  	client, err := c.NewAPIClient()
   170  	if err != nil {
   171  		return err
   172  	}
   173  	defer client.Close()
   174  
   175  	conf, err := getClientConfig(client)
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	if err := c.checkProvider(conf); err != nil {
   181  		return err
   182  	}
   183  
   184  	curl, err := resolveCharmURL(c.CharmName, client, conf)
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	repo, err := charm.InferRepository(curl.Reference(), ctx.AbsPath(c.RepoPath))
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	config.SpecializeCharmRepo(repo, conf)
   195  
   196  	curl, err = addCharmViaAPI(client, ctx, curl, repo)
   197  	if err != nil {
   198  		return block.ProcessBlockedError(err, block.BlockChange)
   199  	}
   200  
   201  	if c.BumpRevision {
   202  		ctx.Infof("--upgrade (or -u) is deprecated and ignored; charms are always deployed with a unique revision.")
   203  	}
   204  
   205  	requestedNetworks, err := networkNamesToTags(parseNetworks(c.Networks))
   206  	if err != nil {
   207  		return err
   208  	}
   209  	// We need to ensure network names are valid below, but we don't need them here.
   210  	_, err = networkNamesToTags(c.Constraints.IncludeNetworks())
   211  	if err != nil {
   212  		return err
   213  	}
   214  	_, err = networkNamesToTags(c.Constraints.ExcludeNetworks())
   215  	if err != nil {
   216  		return err
   217  	}
   218  	haveNetworks := len(requestedNetworks) > 0 || c.Constraints.HaveNetworks()
   219  
   220  	charmInfo, err := client.CharmInfo(curl.String())
   221  	if err != nil {
   222  		return err
   223  	}
   224  
   225  	numUnits := c.NumUnits
   226  	if charmInfo.Meta.Subordinate {
   227  		if !constraints.IsEmpty(&c.Constraints) {
   228  			return errors.New("cannot use --constraints with subordinate service")
   229  		}
   230  		if numUnits == 1 && c.ToMachineSpec == "" {
   231  			numUnits = 0
   232  		} else {
   233  			return errors.New("cannot use --num-units or --to with subordinate service")
   234  		}
   235  	}
   236  	serviceName := c.ServiceName
   237  	if serviceName == "" {
   238  		serviceName = charmInfo.Meta.Name
   239  	}
   240  
   241  	var configYAML []byte
   242  	if c.Config.Path != "" {
   243  		configYAML, err = c.Config.Read(ctx)
   244  		if err != nil {
   245  			return err
   246  		}
   247  	}
   248  	// TODO(axw) rename ServiceDeployWithNetworks to ServiceDeploy,
   249  	// and ServiceDeploy to ServiceDeployLegacy or some such.
   250  	err = client.ServiceDeployWithNetworks(
   251  		curl.String(),
   252  		serviceName,
   253  		numUnits,
   254  		string(configYAML),
   255  		c.Constraints,
   256  		c.ToMachineSpec,
   257  		requestedNetworks,
   258  		c.Storage,
   259  	)
   260  	if params.IsCodeNotImplemented(err) {
   261  		if haveNetworks {
   262  			return errors.New("cannot use --networks/--constraints networks=...: not supported by the API server")
   263  		}
   264  		err = client.ServiceDeploy(
   265  			curl.String(),
   266  			serviceName,
   267  			numUnits,
   268  			string(configYAML),
   269  			c.Constraints,
   270  			c.ToMachineSpec)
   271  	}
   272  	return block.ProcessBlockedError(err, block.BlockChange)
   273  }
   274  
   275  // addCharmViaAPI calls the appropriate client API calls to add the
   276  // given charm URL to state. Also displays the charm URL of the added
   277  // charm on stdout.
   278  func addCharmViaAPI(client *api.Client, ctx *cmd.Context, curl *charm.URL, repo charm.Repository) (*charm.URL, error) {
   279  	if curl.Revision < 0 {
   280  		latest, err := charm.Latest(repo, curl)
   281  		if err != nil {
   282  			return nil, err
   283  		}
   284  		curl = curl.WithRevision(latest)
   285  	}
   286  	switch curl.Schema {
   287  	case "local":
   288  		ch, err := repo.Get(curl)
   289  		if err != nil {
   290  			return nil, err
   291  		}
   292  		stateCurl, err := client.AddLocalCharm(curl, ch)
   293  		if err != nil {
   294  			return nil, err
   295  		}
   296  		curl = stateCurl
   297  	case "cs":
   298  		err := client.AddCharm(curl)
   299  		if err != nil {
   300  			return nil, err
   301  		}
   302  	default:
   303  		return nil, fmt.Errorf("unsupported charm URL schema: %q", curl.Schema)
   304  	}
   305  	ctx.Infof("Added charm %q to the environment.", curl)
   306  	return curl, nil
   307  }
   308  
   309  // parseNetworks returns a list of network names by parsing the
   310  // comma-delimited string value of --networks argument.
   311  func parseNetworks(networksValue string) []string {
   312  	parts := strings.Split(networksValue, ",")
   313  	var networks []string
   314  	for _, part := range parts {
   315  		network := strings.TrimSpace(part)
   316  		if network != "" {
   317  			networks = append(networks, network)
   318  		}
   319  	}
   320  	return networks
   321  }
   322  
   323  // networkNamesToTags returns the given network names converted to
   324  // tags, or an error.
   325  func networkNamesToTags(networks []string) ([]string, error) {
   326  	var tags []string
   327  	for _, network := range networks {
   328  		if !names.IsValidNetwork(network) {
   329  			return nil, fmt.Errorf("%q is not a valid network name", network)
   330  		}
   331  		tags = append(tags, names.NewNetworkTag(network).String())
   332  	}
   333  	return tags, nil
   334  }