github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/environs/sync/sync.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package sync 5 6 import ( 7 "bytes" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "os" 12 "path/filepath" 13 14 "github.com/juju/loggo" 15 "github.com/juju/utils" 16 17 "github.com/juju/juju/environs/filestorage" 18 "github.com/juju/juju/environs/simplestreams" 19 "github.com/juju/juju/environs/storage" 20 envtools "github.com/juju/juju/environs/tools" 21 coretools "github.com/juju/juju/tools" 22 "github.com/juju/juju/version" 23 "github.com/juju/juju/version/ubuntu" 24 ) 25 26 var logger = loggo.GetLogger("juju.environs.sync") 27 28 // SyncContext describes the context for tool synchronization. 29 type SyncContext struct { 30 // Target holds the destination for the tool synchronization 31 Target storage.Storage 32 33 // AllVersions controls the copy of all versions, not only the latest. 34 AllVersions bool 35 36 // Copy tools with major version, if MajorVersion > 0. 37 MajorVersion int 38 39 // Copy tools with minor version, if MinorVersion > 0. 40 MinorVersion int 41 42 // DryRun controls that nothing is copied. Instead it's logged 43 // what would be coppied. 44 DryRun bool 45 46 // Dev controls the copy of development versions as well as released ones. 47 Dev bool 48 49 // Tools are being synced for a public cloud so include mirrors information. 50 Public bool 51 52 // Source, if non-empty, specifies a directory in the local file system 53 // to use as a source. 54 Source string 55 } 56 57 // SyncTools copies the Juju tools tarball from the official bucket 58 // or a specified source directory into the user's environment. 59 func SyncTools(syncContext *SyncContext) error { 60 sourceDataSource, err := selectSourceDatasource(syncContext) 61 if err != nil { 62 return err 63 } 64 65 logger.Infof("listing available tools") 66 if syncContext.MajorVersion == 0 && syncContext.MinorVersion == 0 { 67 syncContext.MajorVersion = version.Current.Major 68 syncContext.MinorVersion = -1 69 if !syncContext.AllVersions { 70 syncContext.MinorVersion = version.Current.Minor 71 } 72 } else if !syncContext.Dev && syncContext.MinorVersion != -1 { 73 // If a major.minor version is specified, we allow dev versions. 74 // If Dev is already true, leave it alone. 75 syncContext.Dev = true 76 } 77 78 released := !syncContext.Dev && !version.Current.IsDev() 79 sourceTools, err := envtools.FindToolsForCloud( 80 []simplestreams.DataSource{sourceDataSource}, simplestreams.CloudSpec{}, 81 syncContext.MajorVersion, syncContext.MinorVersion, coretools.Filter{Released: released}) 82 if err != nil { 83 return err 84 } 85 86 logger.Infof("found %d tools", len(sourceTools)) 87 if !syncContext.AllVersions { 88 var latest version.Number 89 latest, sourceTools = sourceTools.Newest() 90 logger.Infof("found %d recent tools (version %s)", len(sourceTools), latest) 91 } 92 for _, tool := range sourceTools { 93 logger.Debugf("found source tool: %v", tool) 94 } 95 96 logger.Infof("listing target tools storage") 97 targetStorage := syncContext.Target 98 targetTools, err := envtools.ReadList(targetStorage, syncContext.MajorVersion, -1) 99 switch err { 100 case nil, coretools.ErrNoMatches, envtools.ErrNoTools: 101 default: 102 return err 103 } 104 for _, tool := range targetTools { 105 logger.Debugf("found target tool: %v", tool) 106 } 107 108 missing := sourceTools.Exclude(targetTools) 109 logger.Infof("found %d tools in target; %d tools to be copied", len(targetTools), len(missing)) 110 err = copyTools(missing, syncContext, targetStorage) 111 if err != nil { 112 return err 113 } 114 logger.Infof("copied %d tools", len(missing)) 115 116 logger.Infof("generating tools metadata") 117 if !syncContext.DryRun { 118 targetTools = append(targetTools, missing...) 119 writeMirrors := envtools.DoNotWriteMirrors 120 if syncContext.Public { 121 writeMirrors = envtools.WriteMirrors 122 } 123 err = envtools.MergeAndWriteMetadata(targetStorage, targetTools, writeMirrors) 124 if err != nil { 125 return err 126 } 127 } 128 logger.Infof("tools metadata written") 129 return nil 130 } 131 132 // selectSourceDatasource returns a storage reader based on the source setting. 133 func selectSourceDatasource(syncContext *SyncContext) (simplestreams.DataSource, error) { 134 source := syncContext.Source 135 if source == "" { 136 source = envtools.DefaultBaseURL 137 } 138 sourceURL, err := envtools.ToolsURL(source) 139 if err != nil { 140 return nil, err 141 } 142 logger.Infof("using sync tools source: %v", sourceURL) 143 return simplestreams.NewURLDataSource("sync tools source", sourceURL, utils.VerifySSLHostnames), nil 144 } 145 146 // copyTools copies a set of tools from the source to the target. 147 func copyTools(tools []*coretools.Tools, syncContext *SyncContext, dest storage.Storage) error { 148 for _, tool := range tools { 149 logger.Infof("copying %s from %s", tool.Version, tool.URL) 150 if syncContext.DryRun { 151 continue 152 } 153 if err := copyOneToolsPackage(tool, dest); err != nil { 154 return err 155 } 156 } 157 return nil 158 } 159 160 // copyOneToolsPackage copies one tool from the source to the target. 161 func copyOneToolsPackage(tool *coretools.Tools, dest storage.Storage) error { 162 toolsName := envtools.StorageName(tool.Version) 163 logger.Infof("copying %v", toolsName) 164 resp, err := utils.GetValidatingHTTPClient().Get(tool.URL) 165 if err != nil { 166 return err 167 } 168 buf := &bytes.Buffer{} 169 srcFile := resp.Body 170 defer srcFile.Close() 171 tool.SHA256, tool.Size, err = utils.ReadSHA256(io.TeeReader(srcFile, buf)) 172 if err != nil { 173 return err 174 } 175 sizeInKB := (tool.Size + 512) / 1024 176 logger.Infof("downloaded %v (%dkB), uploading", toolsName, sizeInKB) 177 logger.Infof("download %dkB, uploading", sizeInKB) 178 return dest.Put(toolsName, buf, tool.Size) 179 } 180 181 // UploadFunc is the type of Upload, which may be 182 // reassigned to control the behaviour of tools 183 // uploading. 184 type UploadFunc func(stor storage.Storage, forceVersion *version.Number, series ...string) (*coretools.Tools, error) 185 186 // Upload builds whatever version of github.com/juju/juju is in $GOPATH, 187 // uploads it to the given storage, and returns a Tools instance describing 188 // them. If forceVersion is not nil, the uploaded tools bundle will report 189 // the given version number; if any fakeSeries are supplied, additional copies 190 // of the built tools will be uploaded for use by machines of those series. 191 // Juju tools built for one series do not necessarily run on another, but this 192 // func exists only for development use cases. 193 var Upload UploadFunc = upload 194 195 func upload(stor storage.Storage, forceVersion *version.Number, fakeSeries ...string) (*coretools.Tools, error) { 196 builtTools, err := BuildToolsTarball(forceVersion) 197 if err != nil { 198 return nil, err 199 } 200 defer os.RemoveAll(builtTools.Dir) 201 logger.Debugf("Uploading tools for %v", fakeSeries) 202 return SyncBuiltTools(stor, builtTools, fakeSeries...) 203 } 204 205 // cloneToolsForSeries copies the built tools tarball into a tarball for the specified 206 // series and generates corresponding metadata. 207 func cloneToolsForSeries(toolsInfo *BuiltTools, series ...string) error { 208 // Copy the tools to the target storage, recording a Tools struct for each one. 209 var targetTools coretools.List 210 targetTools = append(targetTools, &coretools.Tools{ 211 Version: toolsInfo.Version, 212 Size: toolsInfo.Size, 213 SHA256: toolsInfo.Sha256Hash, 214 }) 215 putTools := func(vers version.Binary) (string, error) { 216 name := envtools.StorageName(vers) 217 src := filepath.Join(toolsInfo.Dir, toolsInfo.StorageName) 218 dest := filepath.Join(toolsInfo.Dir, name) 219 err := utils.CopyFile(dest, src) 220 if err != nil { 221 return "", err 222 } 223 // Append to targetTools the attributes required to write out tools metadata. 224 targetTools = append(targetTools, &coretools.Tools{ 225 Version: vers, 226 Size: toolsInfo.Size, 227 SHA256: toolsInfo.Sha256Hash, 228 }) 229 return name, nil 230 } 231 logger.Debugf("generating tarballs for %v", series) 232 for _, series := range series { 233 _, err := ubuntu.SeriesVersion(series) 234 if err != nil { 235 return err 236 } 237 if series != toolsInfo.Version.Series { 238 fakeVersion := toolsInfo.Version 239 fakeVersion.Series = series 240 if _, err := putTools(fakeVersion); err != nil { 241 return err 242 } 243 } 244 } 245 // The tools have been copied to a temp location from which they will be uploaded, 246 // now write out the matching simplestreams metadata so that SyncTools can find them. 247 metadataStore, err := filestorage.NewFileStorageWriter(toolsInfo.Dir) 248 if err != nil { 249 return err 250 } 251 logger.Debugf("generating tools metadata") 252 return envtools.MergeAndWriteMetadata(metadataStore, targetTools, false) 253 } 254 255 // BuiltTools contains metadata for a tools tarball resulting from 256 // a call to BundleTools. 257 type BuiltTools struct { 258 Version version.Binary 259 Dir string 260 StorageName string 261 Sha256Hash string 262 Size int64 263 } 264 265 // BuildToolsTarballFunc is a function which can build a tools tarball. 266 type BuildToolsTarballFunc func(forceVersion *version.Number) (*BuiltTools, error) 267 268 // Override for testing. 269 var BuildToolsTarball BuildToolsTarballFunc = buildToolsTarball 270 271 // buildToolsTarball bundles a tools tarball and places it in a temp directory in 272 // the expected tools path. 273 func buildToolsTarball(forceVersion *version.Number) (builtTools *BuiltTools, err error) { 274 // TODO(rog) find binaries from $PATH when not using a development 275 // version of juju within a $GOPATH. 276 277 logger.Debugf("Building tools") 278 // We create the entire archive before asking the environment to 279 // start uploading so that we can be sure we have archived 280 // correctly. 281 f, err := ioutil.TempFile("", "juju-tgz") 282 if err != nil { 283 return nil, err 284 } 285 defer f.Close() 286 defer os.Remove(f.Name()) 287 toolsVersion, sha256Hash, err := envtools.BundleTools(f, forceVersion) 288 if err != nil { 289 return nil, err 290 } 291 fileInfo, err := f.Stat() 292 if err != nil { 293 return nil, fmt.Errorf("cannot stat newly made tools archive: %v", err) 294 } 295 size := fileInfo.Size() 296 logger.Infof("built tools %v (%dkB)", toolsVersion, (size+512)/1024) 297 baseToolsDir, err := ioutil.TempDir("", "juju-tools") 298 if err != nil { 299 return nil, err 300 } 301 302 // If we exit with an error, clean up the built tools directory. 303 defer func() { 304 if err != nil { 305 os.RemoveAll(baseToolsDir) 306 } 307 }() 308 309 err = os.MkdirAll(filepath.Join(baseToolsDir, storage.BaseToolsPath, "releases"), 0755) 310 if err != nil { 311 return nil, err 312 } 313 storageName := envtools.StorageName(toolsVersion) 314 err = utils.CopyFile(filepath.Join(baseToolsDir, storageName), f.Name()) 315 if err != nil { 316 return nil, err 317 } 318 return &BuiltTools{ 319 Version: toolsVersion, 320 Dir: baseToolsDir, 321 StorageName: storageName, 322 Size: size, 323 Sha256Hash: sha256Hash, 324 }, nil 325 } 326 327 // SyncBuiltTools copies to storage a tools tarball and cloned copies for each series. 328 func SyncBuiltTools(stor storage.Storage, builtTools *BuiltTools, fakeSeries ...string) (*coretools.Tools, error) { 329 if err := cloneToolsForSeries(builtTools, fakeSeries...); err != nil { 330 return nil, err 331 } 332 syncContext := &SyncContext{ 333 Source: builtTools.Dir, 334 Target: stor, 335 AllVersions: true, 336 Dev: builtTools.Version.IsDev(), 337 MajorVersion: builtTools.Version.Major, 338 MinorVersion: -1, 339 } 340 logger.Debugf("uploading tools to cloud storage") 341 err := SyncTools(syncContext) 342 if err != nil { 343 return nil, err 344 } 345 url, err := stor.URL(builtTools.StorageName) 346 if err != nil { 347 return nil, err 348 } 349 return &coretools.Tools{ 350 Version: builtTools.Version, 351 URL: url, 352 Size: builtTools.Size, 353 SHA256: builtTools.Sha256Hash, 354 }, nil 355 }