Permalink
Fetching contributors…
Cannot retrieve contributors at this time
995 lines (900 sloc) 33.8 KB
// Copyright 2012, 2013 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package commands
import (
"bufio"
"fmt"
"os"
"sort"
"strings"
"github.com/juju/cmd"
"github.com/juju/errors"
"github.com/juju/gnuflag"
"github.com/juju/schema"
"github.com/juju/utils"
"github.com/juju/utils/featureflag"
"github.com/juju/version"
"gopkg.in/juju/charm.v6-unstable"
jujucloud "github.com/juju/juju/cloud"
"github.com/juju/juju/cmd/juju/common"
"github.com/juju/juju/cmd/modelcmd"
"github.com/juju/juju/constraints"
"github.com/juju/juju/controller"
"github.com/juju/juju/environs"
"github.com/juju/juju/environs/bootstrap"
"github.com/juju/juju/environs/config"
"github.com/juju/juju/environs/sync"
"github.com/juju/juju/feature"
"github.com/juju/juju/instance"
"github.com/juju/juju/juju/osenv"
"github.com/juju/juju/jujuclient"
jujuversion "github.com/juju/juju/version"
)
// provisionalProviders is the names of providers that are hidden behind
// feature flags.
var provisionalProviders = map[string]string{
"vsphere": feature.VSphereProvider,
}
var usageBootstrapSummary = `
Initializes a cloud environment.`[1:]
var usageBootstrapDetails = `
Used without arguments, bootstrap will step you through the process of
initializing a Juju cloud environment. Initialization consists of creating
a 'controller' model and provisioning a machine to act as controller.
We recommend you call your controller ‘username-region’ e.g. ‘fred-us-east-1’
See --clouds for a list of clouds and credentials.
See --regions <cloud> for a list of available regions for a given cloud.
Credentials are set beforehand and are distinct from any other
configuration (see `[1:] + "`juju add-credential`" + `).
The 'controller' model typically does not run workloads. It should remain
pristine to run and manage Juju's own infrastructure for the corresponding
cloud. Additional (hosted) models should be created with ` + "`juju create-\nmodel`" + ` for workload purposes.
Note that a 'default' model is also created and becomes the current model
of the environment once the command completes. It can be discarded if
other models are created.
If '--bootstrap-constraints' is used, its values will also apply to any
future controllers provisioned for high availability (HA).
If '--constraints' is used, its values will be set as the default
constraints for all future workload machines in the model, exactly as if
the constraints were set with ` + "`juju set-model-constraints`" + `.
It is possible to override constraints and the automatic machine selection
algorithm by assigning a "placement directive" via the '--to' option. This
dictates what machine to use for the controller. This would typically be
used with the MAAS provider ('--to <host>.maas').
You can change the default timeout and retry delays used during the
bootstrap by changing the following settings in your configuration
(all values represent number of seconds):
# How long to wait for a connection to the controller
bootstrap-timeout: 600 # default: 10 minutes
# How long to wait between connection attempts to a controller
address.
bootstrap-retry-delay: 5 # default: 5 seconds
# How often to refresh controller addresses from the API server.
bootstrap-addresses-delay: 10 # default: 10 seconds
Private clouds may need to specify their own custom image metadata and
tools/agent. Use '--metadata-source' whose value is a local directory.
The value of '--agent-version' will become the default tools version to
use in all models for this controller. The full binary version is accepted
(e.g.: 2.0.1-xenial-amd64) but only the numeric version (e.g.: 2.0.1) is
used. Otherwise, by default, the version used is that of the client.
Examples:
juju bootstrap
juju bootstrap --clouds
juju bootstrap --regions aws
juju bootstrap aws
juju bootstrap aws/us-east-1
juju bootstrap google joe-us-east1
juju bootstrap --config=~/config-rs.yaml rackspace joe-syd
juju bootstrap --config agent-version=1.25.3 aws joe-us-east-1
juju bootstrap --config bootstrap-timeout=1200 azure joe-eastus
See also:
add-credentials
add-model
set-constraints`
// defaultHostedModelName is the name of the hosted model created in each
// controller for deploying workloads to, in addition to the "controller" model.
const defaultHostedModelName = "default"
func newBootstrapCommand() cmd.Command {
return modelcmd.Wrap(
&bootstrapCommand{},
modelcmd.WrapSkipModelFlags, modelcmd.WrapSkipDefaultModel,
)
}
// bootstrapCommand is responsible for launching the first machine in a juju
// environment, and setting up everything necessary to continue working.
type bootstrapCommand struct {
modelcmd.ModelCommandBase
Constraints constraints.Value
ConstraintsStr string
BootstrapConstraints constraints.Value
BootstrapConstraintsStr string
BootstrapSeries string
BootstrapImage string
BuildAgent bool
MetadataSource string
Placement string
KeepBrokenEnvironment bool
AutoUpgrade bool
AgentVersionParam string
AgentVersion *version.Number
config common.ConfigFlag
modelDefaults common.ConfigFlag
showClouds bool
showRegionsForCloud string
controllerName string
hostedModelName string
CredentialName string
Cloud string
Region string
noGUI bool
interactive bool
}
func (c *bootstrapCommand) Info() *cmd.Info {
return &cmd.Info{
Name: "bootstrap",
Args: "[<cloud name>[/region] [<controller name>]]",
Purpose: usageBootstrapSummary,
Doc: usageBootstrapDetails,
}
}
func (c *bootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
c.ModelCommandBase.SetFlags(f)
f.StringVar(&c.ConstraintsStr, "constraints", "", "Set model constraints")
f.StringVar(&c.BootstrapConstraintsStr, "bootstrap-constraints", "", "Specify bootstrap machine constraints")
f.StringVar(&c.BootstrapSeries, "bootstrap-series", "", "Specify the series of the bootstrap machine")
if featureflag.Enabled(feature.ImageMetadata) {
f.StringVar(&c.BootstrapImage, "bootstrap-image", "", "Specify the image of the bootstrap machine")
}
f.BoolVar(&c.BuildAgent, "build-agent", false, "Build local version of agent binary before bootstrapping")
f.StringVar(&c.MetadataSource, "metadata-source", "", "Local path to use as tools and/or metadata source")
f.StringVar(&c.Placement, "to", "", "Placement directive indicating an instance to bootstrap")
f.BoolVar(&c.KeepBrokenEnvironment, "keep-broken", false, "Do not destroy the model if bootstrap fails")
f.BoolVar(&c.AutoUpgrade, "auto-upgrade", false, "Upgrade to the latest patch release tools on first bootstrap")
f.StringVar(&c.AgentVersionParam, "agent-version", "", "Version of tools to use for Juju agents")
f.StringVar(&c.CredentialName, "credential", "", "Credentials to use when bootstrapping")
f.Var(&c.config, "config", "Specify a controller configuration file, or one or more configuration\n options\n (--config config.yaml [--config key=value ...])")
f.Var(&c.modelDefaults, "model-default", "Specify a configuration file, or one or more configuration\n options to be set for all models, unless otherwise specified\n (--config config.yaml [--config key=value ...])")
f.StringVar(&c.hostedModelName, "d", defaultHostedModelName, "Name of the default hosted model for the controller")
f.StringVar(&c.hostedModelName, "default-model", defaultHostedModelName, "Name of the default hosted model for the controller")
f.BoolVar(&c.noGUI, "no-gui", false, "Do not install the Juju GUI in the controller when bootstrapping")
f.BoolVar(&c.showClouds, "clouds", false, "Print the available clouds which can be used to bootstrap a Juju environment")
f.StringVar(&c.showRegionsForCloud, "regions", "", "Print the available regions for the specified cloud")
}
func (c *bootstrapCommand) Init(args []string) (err error) {
if c.showClouds && c.showRegionsForCloud != "" {
return errors.New("--clouds and --regions can't be used together")
}
if c.showClouds {
return cmd.CheckEmpty(args)
}
if c.showRegionsForCloud != "" {
return cmd.CheckEmpty(args)
}
if c.AgentVersionParam != "" && c.BuildAgent {
return errors.New("--agent-version and --build-agent can't be used together")
}
if c.BootstrapSeries != "" && !charm.IsValidSeries(c.BootstrapSeries) {
return errors.NotValidf("series %q", c.BootstrapSeries)
}
// Parse the placement directive. Bootstrap currently only
// supports provider-specific placement directives.
if c.Placement != "" {
_, err = instance.ParsePlacement(c.Placement)
if err != instance.ErrPlacementScopeMissing {
// We only support unscoped placement directives for bootstrap.
return errors.Errorf("unsupported bootstrap placement directive %q", c.Placement)
}
}
if !c.AutoUpgrade {
// With no auto upgrade chosen, we default to the version matching the bootstrap client.
vers := jujuversion.Current
c.AgentVersion = &vers
}
if c.AgentVersionParam != "" {
if vers, err := version.ParseBinary(c.AgentVersionParam); err == nil {
c.AgentVersion = &vers.Number
} else if vers, err := version.Parse(c.AgentVersionParam); err == nil {
c.AgentVersion = &vers
} else {
return err
}
}
if c.AgentVersion != nil && (c.AgentVersion.Major != jujuversion.Current.Major || c.AgentVersion.Minor != jujuversion.Current.Minor) {
return errors.New("requested agent version major.minor mismatch")
}
switch len(args) {
case 0:
// no args or flags, go interactive.
c.interactive = true
return nil
}
c.Cloud = args[0]
if i := strings.IndexRune(c.Cloud, '/'); i > 0 {
c.Cloud, c.Region = c.Cloud[:i], c.Cloud[i+1:]
}
if len(args) > 1 {
c.controllerName = args[1]
return cmd.CheckEmpty(args[2:])
}
return nil
}
// BootstrapInterface provides bootstrap functionality that Run calls to support cleaner testing.
type BootstrapInterface interface {
Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error
CloudRegionDetector(environs.EnvironProvider) (environs.CloudRegionDetector, bool)
}
type bootstrapFuncs struct{}
func (b bootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args bootstrap.BootstrapParams) error {
return bootstrap.Bootstrap(ctx, env, args)
}
func (b bootstrapFuncs) CloudRegionDetector(provider environs.EnvironProvider) (environs.CloudRegionDetector, bool) {
detector, ok := provider.(environs.CloudRegionDetector)
return detector, ok
}
var getBootstrapFuncs = func() BootstrapInterface {
return &bootstrapFuncs{}
}
var (
bootstrapPrepare = bootstrap.Prepare
environsDestroy = environs.Destroy
waitForAgentInitialisation = common.WaitForAgentInitialisation
)
var ambiguousDetectedCredentialError = errors.New(`
more than one credential detected
run juju autoload-credentials and specify a credential using the --credential argument`[1:],
)
var ambiguousCredentialError = errors.New(`
more than one credential is available
specify a credential using the --credential argument`[1:],
)
func (c *bootstrapCommand) parseConstraints(ctx *cmd.Context) (err error) {
allAliases := map[string]string{}
defer common.WarnConstraintAliases(ctx, allAliases)
if c.ConstraintsStr != "" {
cons, aliases, err := constraints.ParseWithAliases(c.ConstraintsStr)
for k, v := range aliases {
allAliases[k] = v
}
if err != nil {
return err
}
c.Constraints = cons
}
if c.BootstrapConstraintsStr != "" {
cons, aliases, err := constraints.ParseWithAliases(c.BootstrapConstraintsStr)
for k, v := range aliases {
allAliases[k] = v
}
if err != nil {
return err
}
c.BootstrapConstraints = cons
}
return nil
}
// Run connects to the environment specified on the command line and bootstraps
// a juju in that environment if none already exists. If there is as yet no environments.yaml file,
// the user is informed how to create one.
func (c *bootstrapCommand) Run(ctx *cmd.Context) (resultErr error) {
if err := c.parseConstraints(ctx); err != nil {
return err
}
// Start by checking for usage errors, equests for information
finished, err := c.handleCommandLineErrorsAndInfoRequests(ctx)
if err != nil {
return errors.Trace(err)
}
if finished {
return nil
}
// Run interactive bootstrap if needed/asked for
if c.interactive {
if err := c.runInteractive(ctx); err != nil {
return errors.Trace(err)
}
// now run normal bootstrap using info gained above.
}
cloud, err := c.getCloud(ctx)
if err != nil {
return errors.Trace(err)
}
provider, err := environs.Provider(cloud.Type)
if err != nil {
return errors.Trace(err)
}
// Custom clouds may not have explicitly declared support for any auth-
// types, in which case we'll assume that they support everything that
// the provider supports.
if len(cloud.AuthTypes) == 0 {
for authType := range provider.CredentialSchemas() {
cloud.AuthTypes = append(cloud.AuthTypes, authType)
}
}
credential,
credentialName,
detectedCredentialName,
regionName,
err := c.getCredentialsAndRegionName(ctx, cloud)
if err != nil {
return errors.Trace(err)
}
region, err := getRegion(cloud, c.Cloud, regionName)
if err != nil {
fmt.Fprintf(ctx.GetStderr(),
"%s\n\nSpecify an alternative region, or try %q.\n",
err, "juju update-clouds",
)
return cmd.ErrSilent
}
if c.controllerName == "" {
c.controllerName = defaultControllerName(c.Cloud, region.Name)
}
bootstrapModelConfig,
controllerConfig,
bootstrapConfig,
inheritedControllerAttrs,
userConfigAttrs,
err := c.getBootstrapConfigs(ctx, cloud, provider)
if err != nil {
return errors.Trace(err)
}
// Read existing current controller so we can clean up on error.
var oldCurrentController string
store := c.ClientStore()
oldCurrentController, err = store.CurrentController()
if errors.IsNotFound(err) {
oldCurrentController = ""
} else if err != nil {
return errors.Annotate(err, "error reading current controller")
}
defer func() {
if resultErr == nil || errors.IsAlreadyExists(resultErr) {
return
}
if oldCurrentController != "" {
if err := store.SetCurrentController(oldCurrentController); err != nil {
logger.Errorf(
"cannot reset current controller to %q: %v",
oldCurrentController, err,
)
}
}
if err := store.RemoveController(c.controllerName); err != nil {
logger.Errorf(
"cannot destroy newly created controller %q details: %v",
c.controllerName, err,
)
}
}()
environ, err := bootstrapPrepare(
modelcmd.BootstrapContext(ctx), store,
bootstrap.PrepareParams{
ModelConfig: bootstrapModelConfig,
ControllerConfig: controllerConfig,
ControllerName: c.controllerName,
Cloud: environs.CloudSpec{
Type: cloud.Type,
Name: c.Cloud,
Region: region.Name,
Endpoint: region.Endpoint,
IdentityEndpoint: region.IdentityEndpoint,
StorageEndpoint: region.StorageEndpoint,
Credential: credential,
},
CredentialName: credentialName,
AdminSecret: bootstrapConfig.AdminSecret,
},
)
if err != nil {
return errors.Trace(err)
}
hostedModelUUID, err := utils.NewUUID()
if err != nil {
return errors.Trace(err)
}
// Set the current model to the initial hosted model.
if err := store.UpdateModel(c.controllerName, c.hostedModelName, jujuclient.ModelDetails{
hostedModelUUID.String(),
}); err != nil {
return errors.Trace(err)
}
if err := store.SetCurrentModel(c.controllerName, c.hostedModelName); err != nil {
return errors.Trace(err)
}
// Set the current controller so "juju status" can be run while
// bootstrapping is underway.
if err := store.SetCurrentController(c.controllerName); err != nil {
return errors.Trace(err)
}
cloudRegion := c.Cloud
if region.Name != "" {
cloudRegion = fmt.Sprintf("%s/%s", cloudRegion, region.Name)
}
ctx.Infof(
"Creating Juju controller %q on %s",
c.controllerName, cloudRegion,
)
// If we error out for any reason, clean up the environment.
defer func() {
if resultErr != nil {
if c.KeepBrokenEnvironment {
ctx.Infof(`
bootstrap failed but --keep-broken was specified so resources are not being destroyed.
When you have finished diagnosing the problem, remember to clean up the failed controller.
See `[1:] + "`juju kill-controller`" + `.`)
} else {
handleBootstrapError(ctx, resultErr, func() error {
return environsDestroy(
c.controllerName, environ, store,
)
})
}
}
}()
// Block interruption during bootstrap. Providers may also
// register for interrupt notification so they can exit early.
interrupted := make(chan os.Signal, 1)
defer close(interrupted)
ctx.InterruptNotify(interrupted)
defer ctx.StopInterruptNotify(interrupted)
go func() {
for _ = range interrupted {
ctx.Infof("Interrupt signalled: waiting for bootstrap to exit")
}
}()
// If --metadata-source is specified, override the default tools metadata source so
// SyncTools can use it, and also upload any image metadata.
var metadataDir string
if c.MetadataSource != "" {
metadataDir = ctx.AbsPath(c.MetadataSource)
}
// Merge environ and bootstrap-specific constraints.
constraintsValidator, err := environ.ConstraintsValidator()
if err != nil {
return errors.Trace(err)
}
bootstrapConstraints, err := constraintsValidator.Merge(
c.Constraints, c.BootstrapConstraints,
)
if err != nil {
return errors.Trace(err)
}
logger.Infof("combined bootstrap constraints: %v", bootstrapConstraints)
hostedModelConfig := c.getHostedModelConfig(hostedModelUUID, inheritedControllerAttrs, userConfigAttrs, environ)
// Check whether the Juju GUI must be installed in the controller.
// Leaving this value empty means no GUI will be installed.
var guiDataSourceBaseURL string
if !c.noGUI {
guiDataSourceBaseURL = common.GUIDataSourceBaseURL()
}
if credentialName == "" {
// credentialName will be empty if the credential was detected.
// We must supply a name for the credential in the database,
// so choose one.
credentialName = detectedCredentialName
}
bootstrapFuncs := getBootstrapFuncs()
err = bootstrapFuncs.Bootstrap(modelcmd.BootstrapContext(ctx), environ, bootstrap.BootstrapParams{
ModelConstraints: c.Constraints,
BootstrapConstraints: bootstrapConstraints,
BootstrapSeries: c.BootstrapSeries,
BootstrapImage: c.BootstrapImage,
Placement: c.Placement,
BuildAgent: c.BuildAgent,
BuildAgentTarball: sync.BuildAgentTarball,
AgentVersion: c.AgentVersion,
MetadataDir: metadataDir,
Cloud: *cloud,
CloudName: c.Cloud,
CloudRegion: region.Name,
CloudCredential: credential,
CloudCredentialName: credentialName,
ControllerConfig: controllerConfig,
ControllerInheritedConfig: inheritedControllerAttrs,
RegionInheritedConfig: cloud.RegionConfig,
HostedModelConfig: hostedModelConfig,
GUIDataSourceBaseURL: guiDataSourceBaseURL,
AdminSecret: bootstrapConfig.AdminSecret,
CAPrivateKey: bootstrapConfig.CAPrivateKey,
DialOpts: environs.BootstrapDialOpts{
Timeout: bootstrapConfig.BootstrapTimeout,
RetryDelay: bootstrapConfig.BootstrapRetryDelay,
AddressesDelay: bootstrapConfig.BootstrapAddressesDelay,
},
})
if err != nil {
return errors.Annotate(err, "failed to bootstrap model")
}
if err := c.SetModelName(modelcmd.JoinModelName(c.controllerName, c.hostedModelName)); err != nil {
return errors.Trace(err)
}
agentVersion := jujuversion.Current
if c.AgentVersion != nil {
agentVersion = *c.AgentVersion
}
err = common.SetBootstrapEndpointAddress(c.ClientStore(), c.controllerName, agentVersion, controllerConfig.APIPort(), environ)
if err != nil {
return errors.Annotate(err, "saving bootstrap endpoint address")
}
// To avoid race conditions when running scripted bootstraps, wait
// for the controller's machine agent to be ready to accept commands
// before exiting this bootstrap command.
return waitForAgentInitialisation(ctx, &c.ModelCommandBase, c.controllerName, c.hostedModelName)
}
func (c *bootstrapCommand) handleCommandLineErrorsAndInfoRequests(ctx *cmd.Context) (bool, error) {
if c.BootstrapImage != "" {
if c.BootstrapSeries == "" {
return true, errors.Errorf("--bootstrap-image must be used with --bootstrap-series")
}
cons, err := constraints.Merge(c.Constraints, c.BootstrapConstraints)
if err != nil {
return true, errors.Trace(err)
}
if !cons.HasArch() {
return true, errors.Errorf("--bootstrap-image must be used with --bootstrap-constraints, specifying architecture")
}
}
if c.showClouds {
return true, printClouds(ctx, c.ClientStore())
}
if c.showRegionsForCloud != "" {
return true, printCloudRegions(ctx, c.showRegionsForCloud)
}
return false, nil
}
func (c *bootstrapCommand) getCloud(ctx *cmd.Context) (*jujucloud.Cloud, error) {
bootstrapFuncs := getBootstrapFuncs()
// Get the cloud definition identified by c.Cloud. If c.Cloud does not
// identify a cloud in clouds.yaml, but is the name of a provider, and
// that provider implements environs.CloudRegionDetector, we'll
// synthesise a Cloud structure with the detected regions and no auth-
// types.
cloud, err := jujucloud.CloudByName(c.Cloud)
if errors.IsNotFound(err) {
ctx.Verbosef("cloud %q not found, trying as a provider name", c.Cloud)
provider, err := environs.Provider(c.Cloud)
if errors.IsNotFound(err) {
return nil, errors.NewNotFound(nil, fmt.Sprintf("unknown cloud %q, please try %q", c.Cloud, "juju update-clouds"))
} else if err != nil {
return nil, errors.Trace(err)
}
detector, ok := bootstrapFuncs.CloudRegionDetector(provider)
if !ok {
ctx.Verbosef(
"provider %q does not support detecting regions",
c.Cloud,
)
return nil, errors.NewNotFound(nil, fmt.Sprintf("unknown cloud %q, please try %q", c.Cloud, "juju update-clouds"))
}
var cloudEndpoint string
regions, err := detector.DetectRegions()
if errors.IsNotFound(err) {
// It's not an error to have no regions. If the
// provider does not support regions, then we
// reinterpret the supplied region name as the
// cloud's endpoint. This enables the user to
// supply, for example, maas/<IP> or manual/<IP>.
if c.Region != "" {
ctx.Verbosef("interpreting %q as the cloud endpoint", c.Region)
cloudEndpoint = c.Region
c.Region = ""
}
} else if err != nil {
return nil, errors.Annotatef(err,
"detecting regions for %q cloud provider",
c.Cloud,
)
}
schemas := provider.CredentialSchemas()
authTypes := make([]jujucloud.AuthType, 0, len(schemas))
for authType := range schemas {
authTypes = append(authTypes, authType)
}
// Since we are iterating over a map, lets sort the authTypes so
// they are always in a consistent order.
sort.Sort(jujucloud.AuthTypes(authTypes))
cloud = &jujucloud.Cloud{
Type: c.Cloud,
AuthTypes: authTypes,
Endpoint: cloudEndpoint,
Regions: regions,
}
} else if err != nil {
return nil, errors.Trace(err)
}
if err := checkProviderType(cloud.Type); errors.IsNotFound(err) {
// This error will get handled later.
} else if err != nil {
return nil, errors.Trace(err)
}
return cloud, nil
}
// Get the credentials and region name.
func (c *bootstrapCommand) getCredentialsAndRegionName(
ctx *cmd.Context, cloud *jujucloud.Cloud) (
*jujucloud.Credential, string, string, string, error) {
store := c.ClientStore()
var detectedCredentialName string
credential, credentialName, regionName, err := modelcmd.GetCredentials(
ctx, store, modelcmd.GetCredentialsParams{
Cloud: *cloud,
CloudName: c.Cloud,
CloudRegion: c.Region,
CredentialName: c.CredentialName,
},
)
if errors.Cause(err) == modelcmd.ErrMultipleCredentials {
return nil, "", "", "", ambiguousCredentialError
}
if errors.IsNotFound(err) && c.CredentialName == "" {
// No credential was explicitly specified, and no credential
// was found in credentials.yaml; have the provider detect
// credentials from the environment.
ctx.Verbosef("no credentials found, checking environment")
detected, err := modelcmd.DetectCredential(c.Cloud, cloud.Type)
if errors.Cause(err) == modelcmd.ErrMultipleCredentials {
return nil, "", "", "", ambiguousDetectedCredentialError
} else if err != nil {
return nil, "", "", "", errors.Trace(err)
}
// We have one credential so extract it from the map.
var oneCredential jujucloud.Credential
for detectedCredentialName, oneCredential = range detected.AuthCredentials {
}
credential = &oneCredential
regionName = c.Region
if regionName == "" {
regionName = detected.DefaultRegion
}
logger.Debugf(
"authenticating with region %q and credential %q (%v)",
regionName, detectedCredentialName, credential.Label,
)
logger.Tracef("credential: %v", credential)
} else if err != nil {
return nil, "", "", "", errors.Trace(err)
}
return credential, credentialName, detectedCredentialName, regionName, nil
}
func (c *bootstrapCommand) getBootstrapConfigs(
ctx *cmd.Context, cloud *jujucloud.Cloud, provider environs.EnvironProvider) (
map[string]interface{}, controller.Config, bootstrap.Config, map[string]interface{}, map[string]interface{}, error) {
controllerModelUUID, err := utils.NewUUID()
if err != nil {
return nil, nil, bootstrap.Config{}, nil, nil, errors.Trace(err)
}
controllerUUID, err := utils.NewUUID()
if err != nil {
return nil, nil, bootstrap.Config{}, nil, nil, errors.Trace(err)
}
// Create a model config, and split out any controller
// and bootstrap config attributes.
combinedConfig := map[string]interface{}{
"type": cloud.Type,
"name": bootstrap.ControllerModelName,
config.UUIDKey: controllerModelUUID.String(),
}
userConfigAttrs, err := c.config.ReadAttrs(ctx)
if err != nil {
return nil, nil, bootstrap.Config{}, nil, nil, errors.Trace(err)
}
modelDefaultConfigAttrs, err := c.modelDefaults.ReadAttrs(ctx)
if err != nil {
return nil, nil, bootstrap.Config{}, nil, nil, errors.Trace(err)
}
// The provider may define some custom attributes specific
// to the provider. These will be added to the model config.
providerAttrs := make(map[string]interface{})
if ps, ok := provider.(config.ConfigSchemaSource); ok {
for attr := range ps.ConfigSchema() {
// Start with the model defaults, and if also specified
// in the user config attrs, they override the model default.
if v, ok := modelDefaultConfigAttrs[attr]; ok {
providerAttrs[attr] = v
}
if v, ok := userConfigAttrs[attr]; ok {
providerAttrs[attr] = v
}
}
fields := schema.FieldMap(ps.ConfigSchema(), ps.ConfigDefaults())
if coercedAttrs, err := fields.Coerce(providerAttrs, nil); err != nil {
return nil, nil, bootstrap.Config{}, nil, nil, errors.Annotatef(err, "invalid attribute value(s) for %v cloud", cloud.Type)
} else {
providerAttrs = coercedAttrs.(map[string]interface{})
}
}
bootstrapConfigAttrs := make(map[string]interface{})
controllerConfigAttrs := make(map[string]interface{})
// Based on the attribute names in clouds.yaml, create
// a map of shared config for all models on this cloud.
inheritedControllerAttrs := make(map[string]interface{})
for k, v := range cloud.Config {
switch {
case bootstrap.IsBootstrapAttribute(k):
bootstrapConfigAttrs[k] = v
continue
case controller.ControllerOnlyAttribute(k):
controllerConfigAttrs[k] = v
continue
}
inheritedControllerAttrs[k] = v
}
// Model defaults are added to the inherited controller attributes.
// Any command line set model defaults override what is in the cloud config.
for k, v := range modelDefaultConfigAttrs {
switch {
case bootstrap.IsBootstrapAttribute(k):
return nil, nil, bootstrap.Config{}, nil, nil, errors.Errorf("%q is a bootstrap only attribute, and cannot be set as a model-default", k)
case controller.ControllerOnlyAttribute(k):
return nil, nil, bootstrap.Config{}, nil, nil, errors.Errorf("%q is a controller attribute, and cannot be set as a model-default", k)
}
inheritedControllerAttrs[k] = v
}
// Start with the model defaults, then add in user config attributes.
for k, v := range modelDefaultConfigAttrs {
combinedConfig[k] = v
}
// Provider specific attributes are either already specified in model
// config (but may have been coerced), or were not present. Either way,
// copy them in.
logger.Debugf("provider attrs: %v", providerAttrs)
for k, v := range providerAttrs {
combinedConfig[k] = v
}
for k, v := range inheritedControllerAttrs {
combinedConfig[k] = v
}
for k, v := range userConfigAttrs {
combinedConfig[k] = v
}
// Add in any default attribute values if not already
// specified, making the recorded bootstrap config
// immutable to changes in Juju.
for k, v := range config.ConfigDefaults() {
if _, ok := combinedConfig[k]; !ok {
combinedConfig[k] = v
}
}
bootstrapModelConfig := make(map[string]interface{})
for k, v := range combinedConfig {
switch {
case bootstrap.IsBootstrapAttribute(k):
bootstrapConfigAttrs[k] = v
case controller.ControllerOnlyAttribute(k):
controllerConfigAttrs[k] = v
default:
bootstrapModelConfig[k] = v
}
}
bootstrapConfig, err := bootstrap.NewConfig(bootstrapConfigAttrs)
if err != nil {
return nil, nil, bootstrap.Config{}, nil, nil, errors.Annotate(err, "constructing bootstrap config")
}
controllerConfig, err := controller.NewConfig(
controllerUUID.String(), bootstrapConfig.CACert, controllerConfigAttrs,
)
if err != nil {
return nil, nil, bootstrap.Config{}, nil, nil, errors.Annotate(err, "constructing controller config")
}
if controllerConfig.AutocertDNSName() != "" {
if _, ok := controllerConfigAttrs[controller.APIPort]; !ok {
// The configuration did not explicitly mention the API port,
// so default to 443 because it is not usually possible to
// obtain autocert certificates without listening on port 443.
controllerConfig[controller.APIPort] = 443
}
}
if err := common.FinalizeAuthorizedKeys(ctx, bootstrapModelConfig); err != nil {
return nil, nil, bootstrap.Config{}, nil, nil, errors.Annotate(err, "finalizing authorized-keys")
}
logger.Debugf("preparing controller with config: %v", bootstrapModelConfig)
return bootstrapModelConfig, controllerConfig, bootstrapConfig, inheritedControllerAttrs, userConfigAttrs, nil
}
func (c *bootstrapCommand) getHostedModelConfig(
hostedModelUUID utils.UUID, inheritedControllerAttrs, userConfigAttrs map[string]interface{}, environ environs.Environ) map[string]interface{} {
hostedModelConfig := map[string]interface{}{
"name": c.hostedModelName,
config.UUIDKey: hostedModelUUID.String(),
}
for k, v := range inheritedControllerAttrs {
hostedModelConfig[k] = v
}
// We copy across any user supplied attributes to the hosted model config.
// But only if the attributes have not been removed from the controller
// model config as part of preparing the controller model.
controllerModelConfigAttrs := environ.Config().AllAttrs()
for k, v := range userConfigAttrs {
if _, ok := controllerModelConfigAttrs[k]; ok {
hostedModelConfig[k] = v
}
}
// Ensure that certain config attributes are not included in the hosted
// model config. These attributes may be modified during bootstrap; by
// removing them from this map, we ensure the modified values are
// inherited.
delete(hostedModelConfig, config.AuthorizedKeysKey)
delete(hostedModelConfig, config.AgentVersionKey)
return hostedModelConfig
}
// runInteractive queries the user about bootstrap config interactively at the
// command prompt.
func (c *bootstrapCommand) runInteractive(ctx *cmd.Context) error {
scanner := bufio.NewScanner(ctx.Stdin)
clouds, err := assembleClouds()
if err != nil {
return errors.Trace(err)
}
c.Cloud, err = queryCloud(clouds, jujucloud.DefaultLXD, scanner, ctx.Stdout)
if err != nil {
return errors.Trace(err)
}
cloud, err := jujucloud.CloudByName(c.Cloud)
if err != nil {
return errors.Trace(err)
}
switch len(cloud.Regions) {
case 0:
// No region to choose, nothing to do.
case 1:
// If there's just one, don't prompt, just use it.
c.Region = cloud.Regions[0].Name
default:
c.Region, err = queryRegion(c.Cloud, cloud.Regions, scanner, ctx.Stdout)
if err != nil {
return errors.Trace(err)
}
}
defName := defaultControllerName(c.Cloud, c.Region)
c.controllerName, err = queryName(defName, scanner, ctx.Stdout)
if err != nil {
return errors.Trace(err)
}
return nil
}
// getRegion returns the cloud.Region to use, based on the specified
// region name. If no region name is specified, and there is at least
// one region, we use the first region in the list.
func getRegion(cloud *jujucloud.Cloud, cloudName, regionName string) (jujucloud.Region, error) {
if regionName != "" {
region, err := jujucloud.RegionByName(cloud.Regions, regionName)
if err != nil {
return jujucloud.Region{}, errors.Trace(err)
}
return *region, nil
}
if len(cloud.Regions) > 0 {
// No region was specified, use the first region in the list.
return cloud.Regions[0], nil
}
return jujucloud.Region{
"", // no region name
cloud.Endpoint,
cloud.IdentityEndpoint,
cloud.StorageEndpoint,
}, nil
}
// checkProviderType ensures the provider type is okay.
func checkProviderType(envType string) error {
featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey)
flag, ok := provisionalProviders[envType]
if ok && !featureflag.Enabled(flag) {
msg := `the %q provider is provisional in this version of Juju. To use it anyway, set JUJU_DEV_FEATURE_FLAGS="%s" in your shell model`
return errors.Errorf(msg, envType, flag)
}
return nil
}
// handleBootstrapError is called to clean up if bootstrap fails.
func handleBootstrapError(ctx *cmd.Context, err error, cleanup func() error) {
ch := make(chan os.Signal, 1)
ctx.InterruptNotify(ch)
defer ctx.StopInterruptNotify(ch)
defer close(ch)
go func() {
for _ = range ch {
fmt.Fprintln(ctx.GetStderr(), "Cleaning up failed bootstrap")
}
}()
if err := cleanup(); err != nil {
logger.Errorf("error cleaning up: %v", err)
}
}