github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/juju/gui/upgradegui.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package gui 5 6 import ( 7 "archive/tar" 8 "compress/bzip2" 9 "crypto/sha256" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "os" 14 "path/filepath" 15 "strings" 16 17 "github.com/juju/cmd" 18 "github.com/juju/errors" 19 "github.com/juju/gnuflag" 20 "github.com/juju/version" 21 22 "github.com/juju/juju/api/controller" 23 "github.com/juju/juju/apiserver/params" 24 "github.com/juju/juju/cmd/juju/common" 25 "github.com/juju/juju/cmd/modelcmd" 26 "github.com/juju/juju/environs/gui" 27 ) 28 29 // NewUpgradeGUICommand creates and returns a new upgrade-gui command. 30 func NewUpgradeGUICommand() cmd.Command { 31 return modelcmd.WrapController(&upgradeGUICommand{}) 32 } 33 34 // upgradeGUICommand upgrades to a new Juju GUI version in the controller. 35 type upgradeGUICommand struct { 36 modelcmd.ControllerCommandBase 37 38 versOrPath string 39 list bool 40 } 41 42 const upgradeGUIDoc = ` 43 Upgrade to the latest Juju GUI released version: 44 45 juju upgrade-gui 46 47 Upgrade to a specific Juju GUI released version: 48 49 juju upgrade-gui 2.2.0 50 51 Upgrade to a Juju GUI version present in a local tar.bz2 GUI release file: 52 53 juju upgrade-gui /path/to/jujugui-2.2.0.tar.bz2 54 55 List available Juju GUI releases without upgrading: 56 57 juju upgrade-gui --list 58 ` 59 60 // Info implements the cmd.Command interface. 61 func (c *upgradeGUICommand) Info() *cmd.Info { 62 return &cmd.Info{ 63 Name: "upgrade-gui", 64 Purpose: "Upgrade to a new Juju GUI version.", 65 Doc: upgradeGUIDoc, 66 } 67 } 68 69 // SetFlags implements the cmd.Command interface. 70 func (c *upgradeGUICommand) SetFlags(f *gnuflag.FlagSet) { 71 c.ControllerCommandBase.SetFlags(f) 72 f.BoolVar(&c.list, "list", false, "List available Juju GUI release versions without upgrading") 73 } 74 75 // Init implements the cmd.Command interface. 76 func (c *upgradeGUICommand) Init(args []string) error { 77 if len(args) == 1 { 78 if c.list { 79 return errors.New("cannot provide arguments if --list is provided") 80 } 81 c.versOrPath = args[0] 82 return nil 83 } 84 return cmd.CheckEmpty(args) 85 } 86 87 // Run implements the cmd.Command interface. 88 func (c *upgradeGUICommand) Run(ctx *cmd.Context) error { 89 if c.list { 90 // List available Juju GUI archive versions. 91 allMeta, err := remoteArchiveMetadata() 92 if err != nil { 93 return errors.Annotate(err, "cannot list Juju GUI release versions") 94 } 95 for _, metadata := range allMeta { 96 ctx.Infof(metadata.Version.String()) 97 } 98 return nil 99 } 100 // Retrieve the GUI archive and its related info. 101 archive, err := openArchive(c.versOrPath) 102 if err != nil { 103 return errors.Trace(err) 104 } 105 defer archive.r.Close() 106 107 // Open the Juju API client. 108 client, err := c.NewControllerAPIClient() 109 if err != nil { 110 return errors.Annotate(err, "cannot establish API connection") 111 } 112 defer client.Close() 113 114 // Check currently uploaded GUI version. 115 existingHash, isCurrent, err := existingVersionInfo(client, archive.vers) 116 if err != nil { 117 return errors.Trace(err) 118 } 119 120 // Upload the release file if required. 121 if archive.hash != existingHash { 122 if archive.local { 123 ctx.Infof("using local Juju GUI archive") 124 } else { 125 ctx.Infof("fetching Juju GUI archive") 126 } 127 f, err := storeArchive(archive.r) 128 if err != nil { 129 return errors.Trace(err) 130 } 131 defer f.Close() 132 ctx.Infof("uploading Juju GUI %s", archive.vers) 133 isCurrent, err = clientUploadGUIArchive(client, f, archive.hash, archive.size, archive.vers) 134 if err != nil { 135 return errors.Annotate(err, "cannot upload Juju GUI") 136 } 137 ctx.Infof("upload completed") 138 } 139 // Switch to the new version if not already at the desired one. 140 if isCurrent { 141 ctx.Infof("Juju GUI at version %s", archive.vers) 142 return nil 143 } 144 if err = clientSelectGUIVersion(client, archive.vers); err != nil { 145 return errors.Annotate(err, "cannot switch to new Juju GUI version") 146 } 147 ctx.Infof("Juju GUI switched to version %s", archive.vers) 148 return nil 149 } 150 151 // openedArchive holds the results of openArchive calls. 152 type openedArchive struct { 153 r io.ReadCloser 154 hash string 155 size int64 156 vers version.Number 157 local bool 158 } 159 160 // openArchive opens a Juju GUI archive from the given version or file path. 161 // The readSeekCloser returned in openedArchive.r must be closed by callers. 162 func openArchive(versOrPath string) (openedArchive, error) { 163 if versOrPath == "" { 164 // Return the most recent Juju GUI from simplestreams. 165 allMeta, err := remoteArchiveMetadata() 166 if err != nil { 167 return openedArchive{}, errors.Annotate(err, "cannot upgrade to most recent release") 168 } 169 // The most recent Juju GUI release is the first on the list. 170 metadata := allMeta[0] 171 r, _, err := metadata.Source.Fetch(metadata.Path) 172 if err != nil { 173 return openedArchive{}, errors.Annotatef(err, "cannot open Juju GUI archive at %q", metadata.FullPath) 174 } 175 return openedArchive{ 176 r: r, 177 hash: metadata.SHA256, 178 size: metadata.Size, 179 vers: metadata.Version, 180 }, nil 181 } 182 f, err := os.Open(versOrPath) 183 if err != nil { 184 if !os.IsNotExist(err) { 185 return openedArchive{}, errors.Annotate(err, "cannot open GUI archive") 186 } 187 vers, err := version.Parse(versOrPath) 188 if err != nil { 189 return openedArchive{}, errors.Errorf("invalid GUI release version or local path %q", versOrPath) 190 } 191 // Return a specific release version from simplestreams. 192 allMeta, err := remoteArchiveMetadata() 193 if err != nil { 194 return openedArchive{}, errors.Annotatef(err, "cannot upgrade to release %s", vers) 195 } 196 metadata, err := findMetadataVersion(allMeta, vers) 197 if err != nil { 198 return openedArchive{}, errors.Trace(err) 199 } 200 r, _, err := metadata.Source.Fetch(metadata.Path) 201 if err != nil { 202 return openedArchive{}, errors.Annotatef(err, "cannot open Juju GUI archive at %q", metadata.FullPath) 203 } 204 return openedArchive{ 205 r: r, 206 hash: metadata.SHA256, 207 size: metadata.Size, 208 vers: metadata.Version, 209 }, nil 210 } 211 // This is a local Juju GUI release. 212 defer func() { 213 if err != nil { 214 f.Close() 215 } 216 }() 217 vers, err := archiveVersion(f) 218 if err != nil { 219 return openedArchive{}, errors.Annotatef(err, "cannot upgrade Juju GUI using %q", versOrPath) 220 } 221 if _, err := f.Seek(0, 0); err != nil { 222 return openedArchive{}, errors.Annotate(err, "cannot seek archive") 223 } 224 hash, size, err := hashAndSize(f) 225 if err != nil { 226 return openedArchive{}, errors.Annotatef(err, "cannot upgrade Juju GUI using %q", versOrPath) 227 } 228 if _, err := f.Seek(0, 0); err != nil { 229 return openedArchive{}, errors.Annotate(err, "cannot seek archive") 230 } 231 return openedArchive{ 232 r: f, 233 hash: hash, 234 size: size, 235 vers: vers, 236 local: true, 237 }, nil 238 } 239 240 // remoteArchiveMetadata returns Juju GUI archive metadata from simplestreams. 241 func remoteArchiveMetadata() ([]*gui.Metadata, error) { 242 source := gui.NewDataSource(common.GUIDataSourceBaseURL()) 243 allMeta, err := guiFetchMetadata(gui.ReleasedStream, source) 244 if err != nil { 245 return nil, errors.Annotate(err, "cannot retrieve Juju GUI archive info") 246 } 247 if len(allMeta) == 0 { 248 return nil, errors.New("no available Juju GUI archives found") 249 } 250 return allMeta, nil 251 } 252 253 // findMetadataVersion returns the metadata in allMeta with the given version. 254 func findMetadataVersion(allMeta []*gui.Metadata, vers version.Number) (*gui.Metadata, error) { 255 for _, metadata := range allMeta { 256 if metadata.Version == vers { 257 return metadata, nil 258 } 259 } 260 return nil, errors.NotFoundf("Juju GUI release version %s", vers) 261 } 262 263 // archiveVersion retrieves the GUI version from the juju-gui-* directory 264 // included in the given tar.bz2 archive reader. 265 func archiveVersion(r io.Reader) (version.Number, error) { 266 var vers version.Number 267 prefix := "jujugui-" 268 tr := tar.NewReader(bzip2.NewReader(r)) 269 for { 270 hdr, err := tr.Next() 271 if err == io.EOF { 272 break 273 } 274 if err != nil { 275 return vers, errors.New("cannot read Juju GUI archive") 276 } 277 info := hdr.FileInfo() 278 if !info.IsDir() || !strings.HasPrefix(hdr.Name, prefix) { 279 continue 280 } 281 n := filepath.Dir(hdr.Name)[len(prefix):] 282 vers, err = version.Parse(n) 283 if err != nil { 284 return vers, errors.Errorf("invalid version %q in archive", n) 285 } 286 return vers, nil 287 } 288 return vers, errors.New("cannot find Juju GUI version in archive") 289 } 290 291 // hashAndSize returns the SHA256 hash and size of the data included in r. 292 func hashAndSize(r io.Reader) (hash string, size int64, err error) { 293 h := sha256.New() 294 size, err = io.Copy(h, r) 295 if err != nil { 296 return "", 0, errors.Annotate(err, "cannot calculate archive hash") 297 } 298 return fmt.Sprintf("%x", h.Sum(nil)), size, nil 299 } 300 301 // existingVersionInfo returns the hash of the existing GUI archive at the 302 // given version and reports whether that's the current version served by the 303 // controller. If the given version is not present in the server, an empty 304 // hash is returned. 305 func existingVersionInfo(client *controller.Client, vers version.Number) (hash string, current bool, err error) { 306 versions, err := clientGUIArchives(client) 307 if err != nil { 308 return "", false, errors.Annotate(err, "cannot retrieve GUI versions from the controller") 309 } 310 for _, v := range versions { 311 if v.Version == vers { 312 return v.SHA256, v.Current, nil 313 } 314 } 315 return "", false, nil 316 } 317 318 // storeArchive saves the Juju GUI archive in the given reader in a temporary 319 // file. The resulting returned readSeekCloser is deleted when closed. 320 func storeArchive(r io.Reader) (readSeekCloser, error) { 321 f, err := ioutil.TempFile("", "gui-archive") 322 if err != nil { 323 return nil, errors.Annotate(err, "cannot create a temporary file to save the Juju GUI archive") 324 } 325 if _, err = io.Copy(f, r); err != nil { 326 return nil, errors.Annotate(err, "cannot retrieve Juju GUI archive") 327 } 328 if _, err = f.Seek(0, 0); err != nil { 329 return nil, errors.Annotate(err, "cannot seek temporary archive file") 330 } 331 return deleteOnCloseFile{f}, nil 332 } 333 334 // readSeekCloser combines the io read, seek and close methods. 335 type readSeekCloser interface { 336 io.ReadCloser 337 io.Seeker 338 } 339 340 // deleteOnCloseFile is a file that gets deleted when closed. 341 type deleteOnCloseFile struct { 342 *os.File 343 } 344 345 // Close closes the file. 346 func (f deleteOnCloseFile) Close() error { 347 f.File.Close() 348 os.Remove(f.Name()) 349 return nil 350 } 351 352 // clientGUIArchives is defined for testing purposes. 353 var clientGUIArchives = func(client *controller.Client) ([]params.GUIArchiveVersion, error) { 354 return client.GUIArchives() 355 } 356 357 // clientSelectGUIVersion is defined for testing purposes. 358 var clientSelectGUIVersion = func(client *controller.Client, vers version.Number) error { 359 return client.SelectGUIVersion(vers) 360 } 361 362 // clientUploadGUIArchive is defined for testing purposes. 363 var clientUploadGUIArchive = func(client *controller.Client, r io.ReadSeeker, hash string, size int64, vers version.Number) (bool, error) { 364 return client.UploadGUIArchive(r, hash, size, vers) 365 } 366 367 // guiFetchMetadata is defined for testing purposes. 368 var guiFetchMetadata = gui.FetchMetadata