launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/agent/tools/toolsdir.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package tools 5 6 import ( 7 "archive/tar" 8 "compress/gzip" 9 "crypto/sha256" 10 "encoding/json" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "os" 15 "path" 16 "strings" 17 18 "github.com/errgo/errgo" 19 20 coretools "launchpad.net/juju-core/tools" 21 "launchpad.net/juju-core/version" 22 ) 23 24 const toolsFile = "downloaded-tools.txt" 25 26 // SharedToolsDir returns the directory that is used to 27 // store binaries for the given version of the juju tools 28 // within the dataDir directory. 29 func SharedToolsDir(dataDir string, vers version.Binary) string { 30 return path.Join(dataDir, "tools", vers.String()) 31 } 32 33 // ToolsDir returns the directory that is used/ to store binaries for 34 // the tools used by the given agent within the given dataDir directory. 35 // Conventionally it is a symbolic link to the actual tools directory. 36 func ToolsDir(dataDir, agentName string) string { 37 return path.Join(dataDir, "tools", agentName) 38 } 39 40 // UnpackTools reads a set of juju tools in gzipped tar-archive 41 // format and unpacks them into the appropriate tools directory 42 // within dataDir. If a valid tools directory already exists, 43 // UnpackTools returns without error. 44 func UnpackTools(dataDir string, tools *coretools.Tools, r io.Reader) (err error) { 45 // Unpack the gzip file and compute the checksum. 46 sha256hash := sha256.New() 47 zr, err := gzip.NewReader(io.TeeReader(r, sha256hash)) 48 if err != nil { 49 return err 50 } 51 defer zr.Close() 52 f, err := ioutil.TempFile(os.TempDir(), "tools-tar") 53 if err != nil { 54 return err 55 } 56 _, err = io.Copy(f, zr) 57 if err != nil { 58 return err 59 } 60 defer os.Remove(f.Name()) 61 // TODO(wallyworld) - 2013-09-24 bug=1229512 62 // When we can ensure all tools records have valid checksums recorded, 63 // we can remove this test short circuit. 64 gzipSHA256 := fmt.Sprintf("%x", sha256hash.Sum(nil)) 65 if tools.SHA256 != "" && tools.SHA256 != gzipSHA256 { 66 return fmt.Errorf("tarball sha256 mismatch, expected %s, got %s", tools.SHA256, gzipSHA256) 67 } 68 69 // Make a temporary directory in the tools directory, 70 // first ensuring that the tools directory exists. 71 toolsDir := path.Join(dataDir, "tools") 72 err = os.MkdirAll(toolsDir, 0755) 73 if err != nil { 74 return err 75 } 76 dir, err := ioutil.TempDir(toolsDir, "unpacking-") 77 if err != nil { 78 return err 79 } 80 defer removeAll(dir) 81 82 // Checksum matches, now reset the file and untar it. 83 _, err = f.Seek(0, 0) 84 if err != nil { 85 return err 86 } 87 tr := tar.NewReader(f) 88 for { 89 hdr, err := tr.Next() 90 if err == io.EOF { 91 break 92 } 93 if err != nil { 94 return err 95 } 96 if strings.ContainsAny(hdr.Name, "/\\") { 97 return fmt.Errorf("bad name %q in tools archive", hdr.Name) 98 } 99 if hdr.Typeflag != tar.TypeReg { 100 return fmt.Errorf("bad file type %c in file %q in tools archive", hdr.Typeflag, hdr.Name) 101 } 102 name := path.Join(dir, hdr.Name) 103 if err := writeFile(name, os.FileMode(hdr.Mode&0777), tr); err != nil { 104 return errgo.Annotatef(err, "tar extract %q failed", name) 105 } 106 } 107 toolsMetadataData, err := json.Marshal(tools) 108 if err != nil { 109 return err 110 } 111 err = ioutil.WriteFile(path.Join(dir, toolsFile), []byte(toolsMetadataData), 0644) 112 if err != nil { 113 return err 114 } 115 116 err = os.Rename(dir, SharedToolsDir(dataDir, tools.Version)) 117 // If we've failed to rename the directory, it may be because 118 // the directory already exists - if ReadTools succeeds, we 119 // assume all's ok. 120 if err != nil { 121 if _, err := ReadTools(dataDir, tools.Version); err == nil { 122 return nil 123 } 124 } 125 return err 126 } 127 128 func removeAll(dir string) { 129 err := os.RemoveAll(dir) 130 if err == nil || os.IsNotExist(err) { 131 return 132 } 133 logger.Warningf("cannot remove %q: %v", dir, err) 134 } 135 136 func writeFile(name string, mode os.FileMode, r io.Reader) error { 137 f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) 138 if err != nil { 139 return err 140 } 141 defer f.Close() 142 _, err = io.Copy(f, r) 143 return err 144 } 145 146 // ReadTools checks that the tools information for the given version exists 147 // in the dataDir directory, and returns a Tools instance. 148 // The tools information is json encoded in a text file, "downloaded-tools.txt". 149 func ReadTools(dataDir string, vers version.Binary) (*coretools.Tools, error) { 150 dir := SharedToolsDir(dataDir, vers) 151 toolsData, err := ioutil.ReadFile(path.Join(dir, toolsFile)) 152 if err != nil { 153 return nil, fmt.Errorf("cannot read tools metadata in tools directory: %v", err) 154 } 155 var tools coretools.Tools 156 if err := json.Unmarshal(toolsData, &tools); err != nil { 157 return nil, fmt.Errorf("invalid tools metadata in tools directory %q: %v", dir, err) 158 } 159 return &tools, nil 160 } 161 162 // ChangeAgentTools atomically replaces the agent-specific symlink 163 // under dataDir so it points to the previously unpacked 164 // version vers. It returns the new tools read. 165 func ChangeAgentTools(dataDir string, agentName string, vers version.Binary) (*coretools.Tools, error) { 166 tools, err := ReadTools(dataDir, vers) 167 if err != nil { 168 return nil, err 169 } 170 tmpName := ToolsDir(dataDir, "tmplink-"+agentName) 171 err = os.Symlink(tools.Version.String(), tmpName) 172 if err != nil { 173 return nil, fmt.Errorf("cannot create tools symlink: %v", err) 174 } 175 err = os.Rename(tmpName, ToolsDir(dataDir, agentName)) 176 if err != nil { 177 return nil, fmt.Errorf("cannot update tools symlink: %v", err) 178 } 179 return tools, nil 180 }