github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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