github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/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/errors" 15 "github.com/juju/loggo" 16 "github.com/juju/utils" 17 jujuseries "github.com/juju/utils/series" 18 19 "github.com/juju/juju/environs/filestorage" 20 "github.com/juju/juju/environs/simplestreams" 21 "github.com/juju/juju/environs/storage" 22 envtools "github.com/juju/juju/environs/tools" 23 coretools "github.com/juju/juju/tools" 24 "github.com/juju/juju/version" 25 ) 26 27 var logger = loggo.GetLogger("juju.environs.sync") 28 29 // SyncContext describes the context for tool synchronization. 30 type SyncContext struct { 31 // TargetToolsFinder is a ToolsFinder provided to find existing 32 // tools in the target destination. 33 TargetToolsFinder ToolsFinder 34 35 // TargetToolsUploader is a ToolsUploader provided to upload 36 // tools to the target destination. 37 TargetToolsUploader ToolsUploader 38 39 // AllVersions controls the copy of all versions, not only the latest. 40 AllVersions bool 41 42 // Copy tools with major version, if MajorVersion > 0. 43 MajorVersion int 44 45 // Copy tools with minor version, if MinorVersion > 0. 46 MinorVersion int 47 48 // DryRun controls that nothing is copied. Instead it's logged 49 // what would be coppied. 50 DryRun bool 51 52 // Stream specifies the simplestreams stream to use (defaults to "Released"). 53 Stream string 54 55 // Source, if non-empty, specifies a directory in the local file system 56 // to use as a source. 57 Source string 58 } 59 60 // ToolsFinder provides an interface for finding tools of a specified version. 61 type ToolsFinder interface { 62 // FindTools returns a list of tools with the specified major version in the specified stream. 63 FindTools(major int, stream string) (coretools.List, error) 64 } 65 66 // ToolsUploader provides an interface for uploading tools and associated 67 // metadata. 68 type ToolsUploader interface { 69 // UploadTools uploads the tools with the specified version and tarball contents. 70 UploadTools(toolsDir, stream string, tools *coretools.Tools, data []byte) error 71 } 72 73 // SyncTools copies the Juju tools tarball from the official bucket 74 // or a specified source directory into the user's environment. 75 func SyncTools(syncContext *SyncContext) error { 76 sourceDataSource, err := selectSourceDatasource(syncContext) 77 if err != nil { 78 return err 79 } 80 81 logger.Infof("listing available tools") 82 if syncContext.MajorVersion == 0 && syncContext.MinorVersion == 0 { 83 syncContext.MajorVersion = version.Current.Major 84 syncContext.MinorVersion = -1 85 if !syncContext.AllVersions { 86 syncContext.MinorVersion = version.Current.Minor 87 } 88 } 89 90 toolsDir := syncContext.Stream 91 // If no stream has been specified, assume "released" for non-devel versions of Juju. 92 if syncContext.Stream == "" { 93 // We now store the tools in a directory named after their stream, but the 94 // legacy behaviour is to store all tools in a single "releases" directory. 95 toolsDir = envtools.LegacyReleaseDirectory 96 syncContext.Stream = envtools.PreferredStream(&version.Current.Number, false, syncContext.Stream) 97 } 98 sourceTools, err := envtools.FindToolsForCloud( 99 []simplestreams.DataSource{sourceDataSource}, simplestreams.CloudSpec{}, 100 syncContext.Stream, syncContext.MajorVersion, syncContext.MinorVersion, coretools.Filter{}) 101 // For backwards compatibility with cloud storage, if there are no tools in the specified stream, 102 // double check the release stream. 103 // TODO - remove this when we no longer need to support cloud storage upgrades. 104 if err == envtools.ErrNoTools { 105 sourceTools, err = envtools.FindToolsForCloud( 106 []simplestreams.DataSource{sourceDataSource}, simplestreams.CloudSpec{}, 107 envtools.ReleasedStream, syncContext.MajorVersion, syncContext.MinorVersion, coretools.Filter{}) 108 } 109 if err != nil { 110 return err 111 } 112 113 logger.Infof("found %d tools", len(sourceTools)) 114 if !syncContext.AllVersions { 115 var latest version.Number 116 latest, sourceTools = sourceTools.Newest() 117 logger.Infof("found %d recent tools (version %s)", len(sourceTools), latest) 118 } 119 for _, tool := range sourceTools { 120 logger.Debugf("found source tool: %v", tool) 121 } 122 123 logger.Infof("listing target tools storage") 124 targetTools, err := syncContext.TargetToolsFinder.FindTools(syncContext.MajorVersion, syncContext.Stream) 125 switch err { 126 case nil, coretools.ErrNoMatches, envtools.ErrNoTools: 127 default: 128 return err 129 } 130 for _, tool := range targetTools { 131 logger.Debugf("found target tool: %v", tool) 132 } 133 134 missing := sourceTools.Exclude(targetTools) 135 logger.Infof("found %d tools in target; %d tools to be copied", len(targetTools), len(missing)) 136 if syncContext.DryRun { 137 for _, tools := range missing { 138 logger.Infof("copying %s from %s", tools.Version, tools.URL) 139 } 140 return nil 141 } 142 143 err = copyTools(toolsDir, syncContext.Stream, missing, syncContext.TargetToolsUploader) 144 if err != nil { 145 return err 146 } 147 logger.Infof("copied %d tools", len(missing)) 148 return nil 149 } 150 151 // selectSourceDatasource returns a storage reader based on the source setting. 152 func selectSourceDatasource(syncContext *SyncContext) (simplestreams.DataSource, error) { 153 source := syncContext.Source 154 if source == "" { 155 source = envtools.DefaultBaseURL 156 } 157 sourceURL, err := envtools.ToolsURL(source) 158 if err != nil { 159 return nil, err 160 } 161 logger.Infof("using sync tools source: %v", sourceURL) 162 return simplestreams.NewURLDataSource("sync tools source", sourceURL, utils.VerifySSLHostnames), nil 163 } 164 165 // copyTools copies a set of tools from the source to the target. 166 func copyTools(toolsDir, stream string, tools []*coretools.Tools, u ToolsUploader) error { 167 for _, tool := range tools { 168 logger.Infof("copying %s from %s", tool.Version, tool.URL) 169 if err := copyOneToolsPackage(toolsDir, stream, tool, u); err != nil { 170 return err 171 } 172 } 173 return nil 174 } 175 176 // copyOneToolsPackage copies one tool from the source to the target. 177 func copyOneToolsPackage(toolsDir, stream string, tools *coretools.Tools, u ToolsUploader) error { 178 toolsName := envtools.StorageName(tools.Version, toolsDir) 179 logger.Infof("downloading %q %v (%v)", stream, toolsName, tools.URL) 180 resp, err := utils.GetValidatingHTTPClient().Get(tools.URL) 181 if err != nil { 182 return err 183 } 184 defer resp.Body.Close() 185 // Verify SHA-256 hash. 186 var buf bytes.Buffer 187 sha256, size, err := utils.ReadSHA256(io.TeeReader(resp.Body, &buf)) 188 if err != nil { 189 return err 190 } 191 if tools.SHA256 == "" { 192 logger.Warningf("no SHA-256 hash for %v", tools.SHA256) 193 } else if sha256 != tools.SHA256 { 194 return errors.Errorf("SHA-256 hash mismatch (%v/%v)", sha256, tools.SHA256) 195 } 196 sizeInKB := (size + 512) / 1024 197 logger.Infof("uploading %v (%dkB) to environment", toolsName, sizeInKB) 198 return u.UploadTools(toolsDir, stream, tools, buf.Bytes()) 199 } 200 201 // UploadFunc is the type of Upload, which may be 202 // reassigned to control the behaviour of tools 203 // uploading. 204 type UploadFunc func(stor storage.Storage, stream string, forceVersion *version.Number, series ...string) (*coretools.Tools, error) 205 206 // Exported for testing. 207 var Upload UploadFunc = upload 208 209 // upload builds whatever version of github.com/juju/juju is in $GOPATH, 210 // uploads it to the given storage, and returns a Tools instance describing 211 // them. If forceVersion is not nil, the uploaded tools bundle will report 212 // the given version number; if any fakeSeries are supplied, additional copies 213 // of the built tools will be uploaded for use by machines of those series. 214 // Juju tools built for one series do not necessarily run on another, but this 215 // func exists only for development use cases. 216 func upload(stor storage.Storage, stream string, forceVersion *version.Number, fakeSeries ...string) (*coretools.Tools, error) { 217 builtTools, err := BuildToolsTarball(forceVersion, stream) 218 if err != nil { 219 return nil, err 220 } 221 defer os.RemoveAll(builtTools.Dir) 222 logger.Debugf("Uploading tools for %v", fakeSeries) 223 return syncBuiltTools(stor, stream, builtTools, fakeSeries...) 224 } 225 226 // cloneToolsForSeries copies the built tools tarball into a tarball for the specified 227 // stream and series and generates corresponding metadata. 228 func cloneToolsForSeries(toolsInfo *BuiltTools, stream string, series ...string) error { 229 // Copy the tools to the target storage, recording a Tools struct for each one. 230 var targetTools coretools.List 231 targetTools = append(targetTools, &coretools.Tools{ 232 Version: toolsInfo.Version, 233 Size: toolsInfo.Size, 234 SHA256: toolsInfo.Sha256Hash, 235 }) 236 putTools := func(vers version.Binary) (string, error) { 237 name := envtools.StorageName(vers, stream) 238 src := filepath.Join(toolsInfo.Dir, toolsInfo.StorageName) 239 dest := filepath.Join(toolsInfo.Dir, name) 240 destDir := filepath.Dir(dest) 241 if err := os.MkdirAll(destDir, 0755); err != nil { 242 return "", err 243 } 244 if err := utils.CopyFile(dest, src); err != nil { 245 return "", err 246 } 247 // Append to targetTools the attributes required to write out tools metadata. 248 targetTools = append(targetTools, &coretools.Tools{ 249 Version: vers, 250 Size: toolsInfo.Size, 251 SHA256: toolsInfo.Sha256Hash, 252 }) 253 return name, nil 254 } 255 logger.Debugf("generating tarballs for %v", series) 256 for _, series := range series { 257 _, err := jujuseries.SeriesVersion(series) 258 if err != nil { 259 return err 260 } 261 if series != toolsInfo.Version.Series { 262 fakeVersion := toolsInfo.Version 263 fakeVersion.Series = series 264 if _, err := putTools(fakeVersion); err != nil { 265 return err 266 } 267 } 268 } 269 // The tools have been copied to a temp location from which they will be uploaded, 270 // now write out the matching simplestreams metadata so that SyncTools can find them. 271 metadataStore, err := filestorage.NewFileStorageWriter(toolsInfo.Dir) 272 if err != nil { 273 return err 274 } 275 logger.Debugf("generating tools metadata") 276 return envtools.MergeAndWriteMetadata(metadataStore, stream, stream, targetTools, false) 277 } 278 279 // BuiltTools contains metadata for a tools tarball resulting from 280 // a call to BundleTools. 281 type BuiltTools struct { 282 Version version.Binary 283 Dir string 284 StorageName string 285 Sha256Hash string 286 Size int64 287 } 288 289 // BuildToolsTarballFunc is a function which can build a tools tarball. 290 type BuildToolsTarballFunc func(forceVersion *version.Number, stream string) (*BuiltTools, error) 291 292 // Override for testing. 293 var BuildToolsTarball BuildToolsTarballFunc = buildToolsTarball 294 295 // buildToolsTarball bundles a tools tarball and places it in a temp directory in 296 // the expected tools path. 297 func buildToolsTarball(forceVersion *version.Number, stream string) (builtTools *BuiltTools, err error) { 298 // TODO(rog) find binaries from $PATH when not using a development 299 // version of juju within a $GOPATH. 300 301 logger.Debugf("Building tools") 302 // We create the entire archive before asking the environment to 303 // start uploading so that we can be sure we have archived 304 // correctly. 305 f, err := ioutil.TempFile("", "juju-tgz") 306 if err != nil { 307 return nil, err 308 } 309 defer f.Close() 310 defer os.Remove(f.Name()) 311 toolsVersion, sha256Hash, err := envtools.BundleTools(f, forceVersion) 312 if err != nil { 313 return nil, err 314 } 315 fileInfo, err := f.Stat() 316 if err != nil { 317 return nil, fmt.Errorf("cannot stat newly made tools archive: %v", err) 318 } 319 size := fileInfo.Size() 320 logger.Infof("built tools %v (%dkB)", toolsVersion, (size+512)/1024) 321 baseToolsDir, err := ioutil.TempDir("", "juju-tools") 322 if err != nil { 323 return nil, err 324 } 325 326 // If we exit with an error, clean up the built tools directory. 327 defer func() { 328 if err != nil { 329 os.RemoveAll(baseToolsDir) 330 } 331 }() 332 333 err = os.MkdirAll(filepath.Join(baseToolsDir, storage.BaseToolsPath, stream), 0755) 334 if err != nil { 335 return nil, err 336 } 337 storageName := envtools.StorageName(toolsVersion, stream) 338 err = utils.CopyFile(filepath.Join(baseToolsDir, storageName), f.Name()) 339 if err != nil { 340 return nil, err 341 } 342 return &BuiltTools{ 343 Version: toolsVersion, 344 Dir: baseToolsDir, 345 StorageName: storageName, 346 Size: size, 347 Sha256Hash: sha256Hash, 348 }, nil 349 } 350 351 // syncBuiltTools copies to storage a tools tarball and cloned copies for each series. 352 func syncBuiltTools(stor storage.Storage, stream string, builtTools *BuiltTools, fakeSeries ...string) (*coretools.Tools, error) { 353 if err := cloneToolsForSeries(builtTools, stream, fakeSeries...); err != nil { 354 return nil, err 355 } 356 syncContext := &SyncContext{ 357 Source: builtTools.Dir, 358 TargetToolsFinder: StorageToolsFinder{stor}, 359 TargetToolsUploader: StorageToolsUploader{stor, false, false}, 360 AllVersions: true, 361 Stream: stream, 362 MajorVersion: builtTools.Version.Major, 363 MinorVersion: -1, 364 } 365 logger.Debugf("uploading tools to cloud storage") 366 err := SyncTools(syncContext) 367 if err != nil { 368 return nil, err 369 } 370 url, err := stor.URL(builtTools.StorageName) 371 if err != nil { 372 return nil, err 373 } 374 return &coretools.Tools{ 375 Version: builtTools.Version, 376 URL: url, 377 Size: builtTools.Size, 378 SHA256: builtTools.Sha256Hash, 379 }, nil 380 } 381 382 // StorageToolsFinder is an implementation of ToolsFinder 383 // that searches for tools in the specified storage. 384 type StorageToolsFinder struct { 385 Storage storage.StorageReader 386 } 387 388 func (f StorageToolsFinder) FindTools(major int, stream string) (coretools.List, error) { 389 return envtools.ReadList(f.Storage, stream, major, -1) 390 } 391 392 // StorageToolsUplader is an implementation of ToolsUploader that 393 // writes tools to the provided storage and then writes merged 394 // metadata, optionally with mirrors. 395 type StorageToolsUploader struct { 396 Storage storage.Storage 397 WriteMetadata bool 398 WriteMirrors envtools.ShouldWriteMirrors 399 } 400 401 func (u StorageToolsUploader) UploadTools(toolsDir, stream string, tools *coretools.Tools, data []byte) error { 402 toolsName := envtools.StorageName(tools.Version, toolsDir) 403 if err := u.Storage.Put(toolsName, bytes.NewReader(data), int64(len(data))); err != nil { 404 return err 405 } 406 if !u.WriteMetadata { 407 return nil 408 } 409 err := envtools.MergeAndWriteMetadata(u.Storage, toolsDir, stream, coretools.List{tools}, u.WriteMirrors) 410 if err != nil { 411 logger.Errorf("error writing tools metadata: %v", err) 412 return err 413 } 414 return nil 415 }