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