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