github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/cmd/juju/system/destroy.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package system 5 6 import ( 7 "bufio" 8 "bytes" 9 "fmt" 10 "io" 11 "strings" 12 "text/tabwriter" 13 14 "github.com/juju/cmd" 15 "github.com/juju/errors" 16 "launchpad.net/gnuflag" 17 18 "github.com/juju/juju/api/systemmanager" 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/environs" 23 "github.com/juju/juju/environs/config" 24 "github.com/juju/juju/environs/configstore" 25 "github.com/juju/juju/juju" 26 ) 27 28 // DestroyCommand destroys the specified system. 29 type DestroyCommand struct { 30 DestroyCommandBase 31 destroyEnvs bool 32 } 33 34 var destroyDoc = `Destroys the specified system` 35 var destroySysMsg = ` 36 WARNING! This command will destroy the %q system. 37 This includes all machines, services, data and other resources. 38 39 Continue [y/N]? `[1:] 40 41 // destroySystemAPI defines the methods on the system manager API endpoint 42 // that the destroy command calls. 43 type destroySystemAPI interface { 44 Close() error 45 EnvironmentConfig() (map[string]interface{}, error) 46 DestroySystem(destroyEnvs bool, ignoreBlocks bool) error 47 ListBlockedEnvironments() ([]params.EnvironmentBlockInfo, error) 48 } 49 50 // destroyClientAPI defines the methods on the client API endpoint that the 51 // destroy command might call. 52 type destroyClientAPI interface { 53 Close() error 54 EnvironmentGet() (map[string]interface{}, error) 55 DestroyEnvironment() error 56 } 57 58 // Info implements Command.Info. 59 func (c *DestroyCommand) Info() *cmd.Info { 60 return &cmd.Info{ 61 Name: "destroy", 62 Args: "<system name>", 63 Purpose: "terminate all machines and other associated resources for a system environment", 64 Doc: destroyDoc, 65 } 66 } 67 68 // SetFlags implements Command.SetFlags. 69 func (c *DestroyCommand) SetFlags(f *gnuflag.FlagSet) { 70 f.BoolVar(&c.destroyEnvs, "destroy-all-environments", false, "destroy all hosted environments on the system") 71 c.DestroyCommandBase.SetFlags(f) 72 } 73 74 func (c *DestroyCommand) getSystemAPI() (destroySystemAPI, error) { 75 if c.api != nil { 76 return c.api, c.apierr 77 } 78 root, err := juju.NewAPIFromName(c.systemName) 79 if err != nil { 80 return nil, errors.Trace(err) 81 } 82 83 return systemmanager.NewClient(root), nil 84 } 85 86 // Run implements Command.Run 87 func (c *DestroyCommand) Run(ctx *cmd.Context) error { 88 store, err := configstore.Default() 89 if err != nil { 90 return errors.Annotate(err, "cannot open system info storage") 91 } 92 93 cfgInfo, err := store.ReadInfo(c.systemName) 94 if err != nil { 95 return errors.Annotate(err, "cannot read system info") 96 } 97 98 // Verify that we're destroying a system 99 apiEndpoint := cfgInfo.APIEndpoint() 100 if apiEndpoint.ServerUUID != "" && apiEndpoint.EnvironUUID != apiEndpoint.ServerUUID { 101 return errors.Errorf("%q is not a system; use juju environment destroy to destroy it", c.systemName) 102 } 103 104 if !c.assumeYes { 105 if err = confirmDestruction(ctx, c.systemName); err != nil { 106 return err 107 } 108 } 109 110 // Attempt to connect to the API. If we can't, fail the destroy. Users will 111 // need to use the system kill command if we can't connect. 112 api, err := c.getSystemAPI() 113 if err != nil { 114 return c.ensureUserFriendlyErrorLog(errors.Annotate(err, "cannot connect to API"), ctx, nil) 115 } 116 defer api.Close() 117 118 // Obtain bootstrap / system environ information 119 systemEnviron, err := c.getSystemEnviron(cfgInfo, api) 120 if err != nil { 121 return errors.Annotate(err, "cannot obtain bootstrap information") 122 } 123 124 // Attempt to destroy the system. 125 err = api.DestroySystem(c.destroyEnvs, false) 126 if params.IsCodeNotImplemented(err) { 127 // Fall back to using the client endpoint to destroy the system, 128 // sending the info we were already able to collect. 129 return c.destroySystemViaClient(ctx, cfgInfo, systemEnviron, store) 130 } 131 if err != nil { 132 return c.ensureUserFriendlyErrorLog(errors.Annotate(err, "cannot destroy system"), ctx, api) 133 } 134 135 return environs.Destroy(systemEnviron, store) 136 } 137 138 // destroySystemViaClient attempts to destroy the system using the client 139 // endpoint for older juju systems which do not implement systemmanager.DestroySystem 140 func (c *DestroyCommand) destroySystemViaClient(ctx *cmd.Context, info configstore.EnvironInfo, systemEnviron environs.Environ, store configstore.Storage) error { 141 api, err := c.getClientAPI() 142 if err != nil { 143 return c.ensureUserFriendlyErrorLog(errors.Annotate(err, "cannot connect to API"), ctx, nil) 144 } 145 defer api.Close() 146 147 err = api.DestroyEnvironment() 148 if err != nil { 149 return c.ensureUserFriendlyErrorLog(errors.Annotate(err, "cannot destroy system"), ctx, nil) 150 } 151 152 return environs.Destroy(systemEnviron, store) 153 } 154 155 // ensureUserFriendlyErrorLog ensures that error will be logged and displayed 156 // in a user-friendly manner with readable and digestable error message. 157 func (c *DestroyCommand) ensureUserFriendlyErrorLog(destroyErr error, ctx *cmd.Context, api destroySystemAPI) error { 158 if destroyErr == nil { 159 return nil 160 } 161 if params.IsCodeOperationBlocked(destroyErr) { 162 logger.Errorf(`there are blocks preventing system destruction 163 To remove all blocks in the system, please run: 164 165 juju system remove-blocks 166 167 `) 168 if api != nil { 169 envs, err := api.ListBlockedEnvironments() 170 var bytes []byte 171 if err == nil { 172 bytes, err = formatTabularBlockedEnvironments(envs) 173 } 174 175 if err != nil { 176 logger.Errorf("Unable to list blocked environments: %s", err) 177 return cmd.ErrSilent 178 } 179 ctx.Infof(string(bytes)) 180 } 181 return cmd.ErrSilent 182 } 183 logger.Errorf(stdFailureMsg, c.systemName) 184 return destroyErr 185 } 186 187 var stdFailureMsg = `failed to destroy system %q 188 189 If the system is unusable, then you may run 190 191 juju system kill 192 193 to forcibly destroy the system. Upon doing so, review 194 your environment provider console for any resources that need 195 to be cleaned up. 196 ` 197 198 func formatTabularBlockedEnvironments(value interface{}) ([]byte, error) { 199 envs, ok := value.([]params.EnvironmentBlockInfo) 200 if !ok { 201 return nil, errors.Errorf("expected value of type %T, got %T", envs, value) 202 } 203 204 var out bytes.Buffer 205 const ( 206 // To format things into columns. 207 minwidth = 0 208 tabwidth = 1 209 padding = 2 210 padchar = ' ' 211 flags = 0 212 ) 213 tw := tabwriter.NewWriter(&out, minwidth, tabwidth, padding, padchar, flags) 214 fmt.Fprintf(tw, "NAME\tENVIRONMENT UUID\tOWNER\tBLOCKS\n") 215 for _, env := range envs { 216 fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", env.Name, env.UUID, env.OwnerTag, blocksToStr(env.Blocks)) 217 } 218 tw.Flush() 219 return out.Bytes(), nil 220 } 221 222 func blocksToStr(blocks []string) string { 223 result := "" 224 sep := "" 225 for _, blk := range blocks { 226 result = result + sep + block.OperationFromType(blk) 227 sep = "," 228 } 229 230 return result 231 } 232 233 // DestroyCommandBase provides common attributes and methods that both the system 234 // destroy and system kill commands require. 235 type DestroyCommandBase struct { 236 envcmd.SysCommandBase 237 systemName string 238 assumeYes bool 239 240 // The following fields are for mocking out 241 // api behavior for testing. 242 api destroySystemAPI 243 apierr error 244 clientapi destroyClientAPI 245 } 246 247 func (c *DestroyCommandBase) getClientAPI() (destroyClientAPI, error) { 248 if c.clientapi != nil { 249 return c.clientapi, nil 250 } 251 root, err := juju.NewAPIFromName(c.systemName) 252 if err != nil { 253 return nil, errors.Trace(err) 254 } 255 return root.Client(), nil 256 } 257 258 // SetFlags implements Command.SetFlags. 259 func (c *DestroyCommandBase) SetFlags(f *gnuflag.FlagSet) { 260 f.BoolVar(&c.assumeYes, "y", false, "Do not ask for confirmation") 261 f.BoolVar(&c.assumeYes, "yes", false, "") 262 } 263 264 // Init implements Command.Init. 265 func (c *DestroyCommandBase) Init(args []string) error { 266 switch len(args) { 267 case 0: 268 return errors.New("no system specified") 269 case 1: 270 c.systemName = args[0] 271 return nil 272 default: 273 return cmd.CheckEmpty(args[1:]) 274 } 275 } 276 277 // getSystemEnviron gets the bootstrap information required to destroy the environment 278 // by first checking the config store, then querying the API if the information is not 279 // in the store. 280 func (c *DestroyCommandBase) getSystemEnviron(info configstore.EnvironInfo, sysAPI destroySystemAPI) (_ environs.Environ, err error) { 281 bootstrapCfg := info.BootstrapConfig() 282 if bootstrapCfg == nil { 283 if sysAPI == nil { 284 return nil, errors.New("unable to get bootstrap information from API") 285 } 286 bootstrapCfg, err = sysAPI.EnvironmentConfig() 287 if params.IsCodeNotImplemented(err) { 288 // Fallback to the client API. Better to encapsulate the logic for 289 // old servers than worry about connecting twice. 290 client, err := c.getClientAPI() 291 if err != nil { 292 return nil, errors.Trace(err) 293 } 294 defer client.Close() 295 bootstrapCfg, err = client.EnvironmentGet() 296 if err != nil { 297 return nil, errors.Trace(err) 298 } 299 } else if err != nil { 300 return nil, errors.Trace(err) 301 } 302 } 303 304 cfg, err := config.New(config.NoDefaults, bootstrapCfg) 305 if err != nil { 306 return nil, errors.Trace(err) 307 } 308 return environs.New(cfg) 309 } 310 311 func confirmDestruction(ctx *cmd.Context, systemName string) error { 312 // Get confirmation from the user that they want to continue 313 fmt.Fprintf(ctx.Stdout, destroySysMsg, systemName) 314 315 scanner := bufio.NewScanner(ctx.Stdin) 316 scanner.Scan() 317 err := scanner.Err() 318 if err != nil && err != io.EOF { 319 return errors.Annotate(err, "system destruction aborted") 320 } 321 answer := strings.ToLower(scanner.Text()) 322 if answer != "y" && answer != "yes" { 323 return errors.New("system destruction aborted") 324 } 325 326 return nil 327 }