github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/cmd/juju/bootstrap.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 10 "github.com/juju/cmd" 11 "github.com/juju/errors" 12 "gopkg.in/juju/charm.v4" 13 "launchpad.net/gnuflag" 14 15 "github.com/juju/juju/cmd/envcmd" 16 "github.com/juju/juju/constraints" 17 "github.com/juju/juju/environs" 18 "github.com/juju/juju/environs/bootstrap" 19 "github.com/juju/juju/instance" 20 "github.com/juju/juju/juju" 21 "github.com/juju/juju/network" 22 "github.com/juju/juju/provider" 23 ) 24 25 const bootstrapDoc = ` 26 bootstrap starts a new environment of the current type (it will return an error 27 if the environment has already been bootstrapped). Bootstrapping an environment 28 will provision a new machine in the environment and run the juju state server on 29 that machine. 30 31 If constraints are specified in the bootstrap command, they will apply to the 32 machine provisioned for the juju state server. They will also be set as default 33 constraints on the environment for all future machines, exactly as if the 34 constraints were set with juju set-constraints. 35 36 It is possible to override constraints and the automatic machine selection 37 algorithm by using the "--to" flag. The value associated with "--to" is a 38 "placement directive", which tells Juju how to identify the first machine to use. 39 For more information on placement directives, see "juju help placement". 40 41 Bootstrap initializes the cloud environment synchronously and displays information 42 about the current installation steps. The time for bootstrap to complete varies 43 across cloud providers from a few seconds to several minutes. Once bootstrap has 44 completed, you can run other juju commands against your environment. You can change 45 the default timeout and retry delays used during the bootstrap by changing the 46 following settings in your environments.yaml (all values represent number of seconds): 47 48 # How long to wait for a connection to the state server. 49 bootstrap-timeout: 600 # default: 10 minutes 50 # How long to wait between connection attempts to a state server address. 51 bootstrap-retry-delay: 5 # default: 5 seconds 52 # How often to refresh state server addresses from the API server. 53 bootstrap-addresses-delay: 10 # default: 10 seconds 54 55 Private clouds may need to specify their own custom image metadata, and possibly upload 56 Juju tools to cloud storage if no outgoing Internet access is available. In this case, 57 use the --metadata-source paramater to tell bootstrap a local directory from which to 58 upload tools and/or image metadata. 59 60 See Also: 61 juju help switch 62 juju help constraints 63 juju help set-constraints 64 juju help placement 65 ` 66 67 // BootstrapCommand is responsible for launching the first machine in a juju 68 // environment, and setting up everything necessary to continue working. 69 type BootstrapCommand struct { 70 envcmd.EnvCommandBase 71 Constraints constraints.Value 72 UploadTools bool 73 Series []string 74 seriesOld []string 75 MetadataSource string 76 Placement string 77 KeepBrokenEnvironment bool 78 } 79 80 func (c *BootstrapCommand) Info() *cmd.Info { 81 return &cmd.Info{ 82 Name: "bootstrap", 83 Purpose: "start up an environment from scratch", 84 Doc: bootstrapDoc, 85 } 86 } 87 88 func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) { 89 f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set environment constraints") 90 f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools before bootstrapping") 91 f.Var(newSeriesValue(nil, &c.Series), "upload-series", "upload tools for supplied comma-separated series list (OBSOLETE)") 92 f.Var(newSeriesValue(nil, &c.seriesOld), "series", "see --upload-series (OBSOLETE)") 93 f.StringVar(&c.MetadataSource, "metadata-source", "", "local path to use as tools and/or metadata source") 94 f.StringVar(&c.Placement, "to", "", "a placement directive indicating an instance to bootstrap") 95 f.BoolVar(&c.KeepBrokenEnvironment, "keep-broken", false, "do not destroy the environment if bootstrap fails") 96 } 97 98 func (c *BootstrapCommand) Init(args []string) (err error) { 99 if len(c.Series) > 0 && !c.UploadTools { 100 return fmt.Errorf("--upload-series requires --upload-tools") 101 } 102 if len(c.seriesOld) > 0 && !c.UploadTools { 103 return fmt.Errorf("--series requires --upload-tools") 104 } 105 if len(c.Series) > 0 && len(c.seriesOld) > 0 { 106 return fmt.Errorf("--upload-series and --series can't be used together") 107 } 108 109 // Parse the placement directive. Bootstrap currently only 110 // supports provider-specific placement directives. 111 if c.Placement != "" { 112 _, err = instance.ParsePlacement(c.Placement) 113 if err != instance.ErrPlacementScopeMissing { 114 // We only support unscoped placement directives for bootstrap. 115 return fmt.Errorf("unsupported bootstrap placement directive %q", c.Placement) 116 } 117 } 118 return cmd.CheckEmpty(args) 119 } 120 121 type seriesValue struct { 122 *cmd.StringsValue 123 } 124 125 // newSeriesValue is used to create the type passed into the gnuflag.FlagSet Var function. 126 func newSeriesValue(defaultValue []string, target *[]string) *seriesValue { 127 v := seriesValue{(*cmd.StringsValue)(target)} 128 *(v.StringsValue) = defaultValue 129 return &v 130 } 131 132 // Implements gnuflag.Value Set. 133 func (v *seriesValue) Set(s string) error { 134 if err := v.StringsValue.Set(s); err != nil { 135 return err 136 } 137 for _, name := range *(v.StringsValue) { 138 if !charm.IsValidSeries(name) { 139 v.StringsValue = nil 140 return fmt.Errorf("invalid series name %q", name) 141 } 142 } 143 return nil 144 } 145 146 // bootstrap functionality that Run calls to support cleaner testing 147 type BootstrapInterface interface { 148 EnsureNotBootstrapped(env environs.Environ) error 149 Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error 150 } 151 152 type bootstrapFuncs struct{} 153 154 func (b bootstrapFuncs) EnsureNotBootstrapped(env environs.Environ) error { 155 return bootstrap.EnsureNotBootstrapped(env) 156 } 157 158 func (b bootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args bootstrap.BootstrapParams) error { 159 return bootstrap.Bootstrap(ctx, env, args) 160 } 161 162 var getBootstrapFuncs = func() BootstrapInterface { 163 return &bootstrapFuncs{} 164 } 165 166 // Run connects to the environment specified on the command line and bootstraps 167 // a juju in that environment if none already exists. If there is as yet no environments.yaml file, 168 // the user is informed how to create one. 169 func (c *BootstrapCommand) Run(ctx *cmd.Context) (resultErr error) { 170 bootstrapFuncs := getBootstrapFuncs() 171 172 if len(c.seriesOld) > 0 { 173 fmt.Fprintln(ctx.Stderr, "Use of --series is obsolete. --upload-tools now expands to all supported series of the same operating system.") 174 } 175 if len(c.Series) > 0 { 176 fmt.Fprintln(ctx.Stderr, "Use of --upload-series is obsolete. --upload-tools now expands to all supported series of the same operating system.") 177 } 178 179 if c.ConnectionName() == "" { 180 return fmt.Errorf("the name of the environment must be specified") 181 } 182 183 environ, cleanup, err := environFromName( 184 ctx, 185 c.ConnectionName(), 186 "Bootstrap", 187 bootstrapFuncs.EnsureNotBootstrapped, 188 ) 189 190 // If we error out for any reason, clean up the environment. 191 defer func() { 192 if resultErr != nil && cleanup != nil { 193 if c.KeepBrokenEnvironment { 194 logger.Warningf("bootstrap failed but --keep-broken was specified so environment is not being destroyed.\n" + 195 "When you are finished diagnosing the problem, remember to run juju destroy-environment --force\n" + 196 "to clean up the environment.") 197 } else { 198 handleBootstrapError(ctx, resultErr, cleanup) 199 } 200 } 201 }() 202 203 // Handle any errors from environFromName(...). 204 if err != nil { 205 return errors.Annotatef(err, "there was an issue examining the environment") 206 } 207 208 // Check to see if this environment is already bootstrapped. If it 209 // is, we inform the user and exit early. If an error is returned 210 // but it is not that the environment is already bootstrapped, 211 // then we're in an unknown state. 212 if err := bootstrapFuncs.EnsureNotBootstrapped(environ); nil != err { 213 if environs.ErrAlreadyBootstrapped == err { 214 logger.Warningf("This juju environment is already bootstrapped. If you want to start a new Juju\nenvironment, first run juju destroy-environment to clean up, or switch to an\nalternative environment.") 215 return err 216 } 217 return errors.Annotatef(err, "cannot determine if environment is already bootstrapped.") 218 } 219 220 // Block interruption during bootstrap. Providers may also 221 // register for interrupt notification so they can exit early. 222 interrupted := make(chan os.Signal, 1) 223 defer close(interrupted) 224 ctx.InterruptNotify(interrupted) 225 defer ctx.StopInterruptNotify(interrupted) 226 go func() { 227 for _ = range interrupted { 228 ctx.Infof("Interrupt signalled: waiting for bootstrap to exit") 229 } 230 }() 231 232 // If --metadata-source is specified, override the default tools metadata source so 233 // SyncTools can use it, and also upload any image metadata. 234 var metadataDir string 235 if c.MetadataSource != "" { 236 metadataDir = ctx.AbsPath(c.MetadataSource) 237 } 238 239 // TODO (wallyworld): 2013-09-20 bug 1227931 240 // We can set a custom tools data source instead of doing an 241 // unnecessary upload. 242 if environ.Config().Type() == provider.Local { 243 c.UploadTools = true 244 } 245 246 err = bootstrapFuncs.Bootstrap(envcmd.BootstrapContext(ctx), environ, bootstrap.BootstrapParams{ 247 Constraints: c.Constraints, 248 Placement: c.Placement, 249 UploadTools: c.UploadTools, 250 MetadataDir: metadataDir, 251 }) 252 if err != nil { 253 return errors.Annotate(err, "failed to bootstrap environment") 254 } 255 return c.SetBootstrapEndpointAddress(environ) 256 } 257 258 // handleBootstrapError is called to clean up if bootstrap fails. 259 func handleBootstrapError(ctx *cmd.Context, err error, cleanup func()) { 260 ch := make(chan os.Signal, 1) 261 ctx.InterruptNotify(ch) 262 defer ctx.StopInterruptNotify(ch) 263 defer close(ch) 264 go func() { 265 for _ = range ch { 266 fmt.Fprintln(ctx.GetStderr(), "Cleaning up failed bootstrap") 267 } 268 }() 269 cleanup() 270 } 271 272 var allInstances = func(environ environs.Environ) ([]instance.Instance, error) { 273 return environ.AllInstances() 274 } 275 276 var prepareEndpointsForCaching = juju.PrepareEndpointsForCaching 277 278 // SetBootstrapEndpointAddress writes the API endpoint address of the 279 // bootstrap server into the connection information. This should only be run 280 // once directly after Bootstrap. It assumes that there is just one instance 281 // in the environment - the bootstrap instance. 282 func (c *BootstrapCommand) SetBootstrapEndpointAddress(environ environs.Environ) error { 283 instances, err := allInstances(environ) 284 if err != nil { 285 return errors.Trace(err) 286 } 287 length := len(instances) 288 if length == 0 { 289 return errors.Errorf("found no instances, expected at least one") 290 } 291 if length > 1 { 292 logger.Warningf("expected one instance, got %d", length) 293 } 294 bootstrapInstance := instances[0] 295 cfg := environ.Config() 296 info, err := envcmd.ConnectionInfoForName(c.ConnectionName()) 297 if err != nil { 298 return errors.Annotate(err, "failed to get connection info") 299 } 300 301 // Don't use c.ConnectionEndpoint as it attempts to contact the state 302 // server if no addresses are found in connection info. 303 endpoint := info.APIEndpoint() 304 netAddrs, err := bootstrapInstance.Addresses() 305 if err != nil { 306 return errors.Annotate(err, "failed to get bootstrap instance addresses") 307 } 308 apiPort := cfg.APIPort() 309 apiHostPorts := network.AddressesWithPort(netAddrs, apiPort) 310 addrs, hosts, addrsChanged := prepareEndpointsForCaching( 311 info, [][]network.HostPort{apiHostPorts}, network.HostPort{}, 312 ) 313 if !addrsChanged { 314 // Something's wrong we already have cached addresses? 315 return errors.Annotate(err, "cached API endpoints unexpectedly exist") 316 } 317 endpoint.Addresses = addrs 318 endpoint.Hostnames = hosts 319 writer, err := c.ConnectionWriter() 320 if err != nil { 321 return errors.Annotate(err, "failed to get connection writer") 322 } 323 writer.SetAPIEndpoint(endpoint) 324 err = writer.Write() 325 if err != nil { 326 return errors.Annotate(err, "failed to write API endpoint to connection info") 327 } 328 return nil 329 }