github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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/version" 20 "launchpad.net/gnuflag" 21 22 "github.com/juju/juju/api" 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.Wrap(&upgradeGUICommand{}) 32 } 33 34 // upgradeGUICommand upgrades to a new Juju GUI version in the controller. 35 type upgradeGUICommand struct { 36 modelcmd.ModelCommandBase 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 f.BoolVar(&c.list, "list", false, "list available Juju GUI release versions without upgrading") 72 } 73 74 // Init implements the cmd.Command interface. 75 func (c *upgradeGUICommand) Init(args []string) error { 76 if len(args) == 1 { 77 if c.list { 78 return errors.New("cannot provide arguments if --list is provided") 79 } 80 c.versOrPath = args[0] 81 return nil 82 } 83 return cmd.CheckEmpty(args) 84 } 85 86 // Run implements the cmd.Command interface. 87 func (c *upgradeGUICommand) Run(ctx *cmd.Context) error { 88 if c.list { 89 // List available Juju GUI archive versions. 90 allMeta, err := remoteArchiveMetadata() 91 if err != nil { 92 return errors.Annotate(err, "cannot list Juju GUI release versions") 93 } 94 for _, metadata := range allMeta { 95 ctx.Infof(metadata.Version.String()) 96 } 97 return nil 98 } 99 // Retrieve the GUI archive and its related info. 100 r, hash, size, vers, err := openArchive(c.versOrPath) 101 if err != nil { 102 return errors.Trace(err) 103 } 104 defer r.Close() 105 106 // Open the Juju API client. 107 client, err := c.NewAPIClient() 108 if err != nil { 109 return errors.Annotate(err, "cannot establish API connection") 110 } 111 defer client.Close() 112 113 // Check currently uploaded GUI version. 114 existingHash, isCurrent, err := existingVersionInfo(client, vers) 115 if err != nil { 116 return errors.Trace(err) 117 } 118 119 // Upload the release file if required. 120 if hash != existingHash { 121 ctx.Infof("fetching Juju GUI archive") 122 f, err := storeArchive(r) 123 if err != nil { 124 return errors.Trace(err) 125 } 126 defer f.Close() 127 ctx.Infof("uploading Juju GUI %s", vers) 128 isCurrent, err = clientUploadGUIArchive(client, f, hash, size, vers) 129 if err != nil { 130 return errors.Annotate(err, "cannot upload Juju GUI") 131 } 132 ctx.Infof("upload completed") 133 } 134 // Switch to the new version if not already at the desired one. 135 if isCurrent { 136 ctx.Infof("Juju GUI at version %s", vers) 137 return nil 138 } 139 if err = clientSelectGUIVersion(client, vers); err != nil { 140 return errors.Annotate(err, "cannot switch to new Juju GUI version") 141 } 142 ctx.Infof("Juju GUI switched to version %s", vers) 143 return nil 144 } 145 146 // openArchive opens a Juju GUI archive from the given version or file path. 147 // The returned readSeekCloser must be closed by callers. 148 func openArchive(versOrPath string) (r io.ReadCloser, hash string, size int64, vers version.Number, err error) { 149 if versOrPath == "" { 150 // Return the most recent Juju GUI from simplestreams. 151 allMeta, err := remoteArchiveMetadata() 152 if err != nil { 153 return nil, "", 0, vers, errors.Annotate(err, "cannot upgrade to most recent release") 154 } 155 // The most recent Juju GUI release is the first on the list. 156 metadata := allMeta[0] 157 r, _, err := metadata.Source.Fetch(metadata.Path) 158 if err != nil { 159 return nil, "", 0, vers, errors.Annotatef(err, "cannot open Juju GUI archive at %q", metadata.FullPath) 160 } 161 return r, metadata.SHA256, metadata.Size, metadata.Version, nil 162 } 163 f, err := os.Open(versOrPath) 164 if err != nil { 165 if !os.IsNotExist(err) { 166 return nil, "", 0, vers, errors.Annotate(err, "cannot open GUI archive") 167 } 168 vers, err = version.Parse(versOrPath) 169 if err != nil { 170 return nil, "", 0, vers, errors.Errorf("invalid GUI release version or local path %q", versOrPath) 171 } 172 // Return a specific release version from simplestreams. 173 allMeta, err := remoteArchiveMetadata() 174 if err != nil { 175 return nil, "", 0, vers, errors.Annotatef(err, "cannot upgrade to release %s", vers) 176 } 177 metadata, err := findMetadataVersion(allMeta, vers) 178 if err != nil { 179 return nil, "", 0, vers, errors.Trace(err) 180 } 181 r, _, err := metadata.Source.Fetch(metadata.Path) 182 if err != nil { 183 return nil, "", 0, vers, errors.Annotatef(err, "cannot open Juju GUI archive at %q", metadata.FullPath) 184 } 185 return r, metadata.SHA256, metadata.Size, metadata.Version, nil 186 } 187 // This is a local Juju GUI release. 188 defer func() { 189 if err != nil { 190 f.Close() 191 } 192 }() 193 vers, err = archiveVersion(f) 194 if err != nil { 195 return nil, "", 0, vers, errors.Annotatef(err, "cannot upgrade Juju GUI using %q", versOrPath) 196 } 197 if _, err := f.Seek(0, 0); err != nil { 198 return nil, "", 0, version.Number{}, errors.Annotate(err, "cannot seek archive") 199 } 200 hash, size, err = hashAndSize(f) 201 if err != nil { 202 return nil, "", 0, version.Number{}, errors.Annotatef(err, "cannot upgrade Juju GUI using %q", versOrPath) 203 } 204 if _, err := f.Seek(0, 0); err != nil { 205 return nil, "", 0, version.Number{}, errors.Annotate(err, "cannot seek archive") 206 } 207 return f, hash, size, vers, nil 208 } 209 210 // remoteArchiveMetadata returns Juju GUI archive metadata from simplestreams. 211 func remoteArchiveMetadata() ([]*gui.Metadata, error) { 212 source := gui.NewDataSource(common.GUIDataSourceBaseURL()) 213 allMeta, err := guiFetchMetadata(gui.ReleasedStream, source) 214 if err != nil { 215 return nil, errors.Annotate(err, "cannot retrieve Juju GUI archive info") 216 } 217 if len(allMeta) == 0 { 218 return nil, errors.New("no available Juju GUI archives found") 219 } 220 return allMeta, nil 221 } 222 223 // findMetadataVersion returns the metadata in allMeta with the given version. 224 func findMetadataVersion(allMeta []*gui.Metadata, vers version.Number) (*gui.Metadata, error) { 225 for _, metadata := range allMeta { 226 if metadata.Version == vers { 227 return metadata, nil 228 } 229 } 230 return nil, errors.NotFoundf("Juju GUI release version %s", vers) 231 } 232 233 // archiveVersion retrieves the GUI version from the juju-gui-* directory 234 // included in the given tar.bz2 archive reader. 235 func archiveVersion(r io.Reader) (version.Number, error) { 236 var vers version.Number 237 prefix := "jujugui-" 238 tr := tar.NewReader(bzip2.NewReader(r)) 239 for { 240 hdr, err := tr.Next() 241 if err == io.EOF { 242 break 243 } 244 if err != nil { 245 return vers, errors.New("cannot read Juju GUI archive") 246 } 247 info := hdr.FileInfo() 248 if !info.IsDir() || !strings.HasPrefix(hdr.Name, prefix) { 249 continue 250 } 251 n := filepath.Dir(hdr.Name)[len(prefix):] 252 vers, err = version.Parse(n) 253 if err != nil { 254 return vers, errors.Errorf("invalid version %q in archive", n) 255 } 256 return vers, nil 257 } 258 return vers, errors.New("cannot find Juju GUI version in archive") 259 } 260 261 // hashAndSize returns the SHA256 hash and size of the data included in r. 262 func hashAndSize(r io.Reader) (hash string, size int64, err error) { 263 h := sha256.New() 264 size, err = io.Copy(h, r) 265 if err != nil { 266 return "", 0, errors.Annotate(err, "cannot calculate archive hash") 267 } 268 return fmt.Sprintf("%x", h.Sum(nil)), size, nil 269 } 270 271 // existingVersionInfo returns the hash of the existing GUI archive at the 272 // given version and reports whether that's the current version served by the 273 // controller. If the given version is not present in the server, an empty 274 // hash is returned. 275 func existingVersionInfo(client *api.Client, vers version.Number) (hash string, current bool, err error) { 276 versions, err := clientGUIArchives(client) 277 if err != nil { 278 return "", false, errors.Annotate(err, "cannot retrieve GUI versions from the controller") 279 } 280 for _, v := range versions { 281 if v.Version == vers { 282 return v.SHA256, v.Current, nil 283 } 284 } 285 return "", false, nil 286 } 287 288 // storeArchive saves the Juju GUI archive in the given reader in a temporary 289 // file. The resulting returned readSeekCloser is deleted when closed. 290 func storeArchive(r io.Reader) (readSeekCloser, error) { 291 f, err := ioutil.TempFile("", "gui-archive") 292 if err != nil { 293 return nil, errors.Annotate(err, "cannot create a temporary file to save the Juju GUI archive") 294 } 295 if _, err = io.Copy(f, r); err != nil { 296 return nil, errors.Annotate(err, "cannot retrieve Juju GUI archive") 297 } 298 if _, err = f.Seek(0, 0); err != nil { 299 return nil, errors.Annotate(err, "cannot seek temporary archive file") 300 } 301 return deleteOnCloseFile{f}, nil 302 } 303 304 // readSeekCloser combines the io read, seek and close methods. 305 type readSeekCloser interface { 306 io.ReadCloser 307 io.Seeker 308 } 309 310 // deleteOnCloseFile is a file that gets deleted when closed. 311 type deleteOnCloseFile struct { 312 *os.File 313 } 314 315 // Close closes the file. 316 func (f deleteOnCloseFile) Close() error { 317 f.File.Close() 318 os.Remove(f.Name()) 319 return nil 320 } 321 322 // clientGUIArchives is defined for testing purposes. 323 var clientGUIArchives = func(client *api.Client) ([]params.GUIArchiveVersion, error) { 324 return client.GUIArchives() 325 } 326 327 // clientSelectGUIVersion is defined for testing purposes. 328 var clientSelectGUIVersion = func(client *api.Client, vers version.Number) error { 329 return client.SelectGUIVersion(vers) 330 } 331 332 // clientUploadGUIArchive is defined for testing purposes. 333 var clientUploadGUIArchive = func(client *api.Client, r io.ReadSeeker, hash string, size int64, vers version.Number) (bool, error) { 334 return client.UploadGUIArchive(r, hash, size, vers) 335 } 336 337 // guiFetchMetadata is defined for testing purposes. 338 var guiFetchMetadata = gui.FetchMetadata