github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 jujuseries "github.com/juju/os/series" 16 "github.com/juju/utils" 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 errors.Trace(err) 80 } 81 82 logger.Infof("listing available agent binaries") 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.ReleasedStream 97 // Always use the primary stream here - the user can specify 98 // to override that decision. 99 syncContext.Stream = envtools.PreferredStreams(&jujuversion.Current, false, "")[0] 100 } 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 streams := []string{syncContext.Stream, envtools.ReleasedStream} 105 sourceTools, err := envtools.FindToolsForCloud( 106 []simplestreams.DataSource{sourceDataSource}, simplestreams.CloudSpec{}, 107 streams, syncContext.MajorVersion, syncContext.MinorVersion, coretools.Filter{}) 108 if err != nil { 109 return errors.Trace(err) 110 } 111 112 logger.Infof("found %d agent binaries", len(sourceTools)) 113 if !syncContext.AllVersions { 114 var latest version.Number 115 latest, sourceTools = sourceTools.Newest() 116 logger.Infof("found %d recent agent binaries (version %s)", len(sourceTools), latest) 117 } 118 for _, tool := range sourceTools { 119 logger.Debugf("found source agent binary: %v", tool) 120 } 121 122 logger.Infof("listing target agent binaries storage") 123 targetTools, err := syncContext.TargetToolsFinder.FindTools(syncContext.MajorVersion, syncContext.Stream) 124 switch err { 125 case nil, coretools.ErrNoMatches, envtools.ErrNoTools: 126 default: 127 return errors.Trace(err) 128 } 129 for _, tool := range targetTools { 130 logger.Debugf("found target agent binary: %v", tool) 131 } 132 133 missing := sourceTools.Exclude(targetTools) 134 logger.Infof("found %d agent binaries in target; %d agent binaries to be copied", len(targetTools), len(missing)) 135 if syncContext.DryRun { 136 for _, tools := range missing { 137 logger.Infof("copying %s from %s", tools.Version, tools.URL) 138 } 139 return nil 140 } 141 142 err = copyTools(toolsDir, syncContext.Stream, missing, syncContext.TargetToolsUploader) 143 if err != nil { 144 return err 145 } 146 logger.Infof("copied %d agent binaries", len(missing)) 147 return nil 148 } 149 150 // selectSourceDatasource returns a storage reader based on the source setting. 151 func selectSourceDatasource(syncContext *SyncContext) (simplestreams.DataSource, error) { 152 source := syncContext.Source 153 if source == "" { 154 source = envtools.DefaultBaseURL 155 } 156 sourceURL, err := envtools.ToolsURL(source) 157 if err != nil { 158 return nil, err 159 } 160 logger.Infof("source for sync of agent binaries: %v", sourceURL) 161 return simplestreams.NewURLSignedDataSource("sync agent binaries source", sourceURL, keys.JujuPublicKey, utils.VerifySSLHostnames, simplestreams.CUSTOM_CLOUD_DATA, false), nil 162 } 163 164 // copyTools copies a set of tools from the source to the target. 165 func copyTools(toolsDir, stream string, tools []*coretools.Tools, u ToolsUploader) error { 166 for _, tool := range tools { 167 logger.Infof("copying %s from %s", tool.Version, tool.URL) 168 if err := copyOneToolsPackage(toolsDir, stream, tool, u); err != nil { 169 return err 170 } 171 } 172 return nil 173 } 174 175 // copyOneToolsPackage copies one tool from the source to the target. 176 func copyOneToolsPackage(toolsDir, stream string, tools *coretools.Tools, u ToolsUploader) error { 177 toolsName := envtools.StorageName(tools.Version, toolsDir) 178 logger.Infof("downloading %q %v (%v)", stream, toolsName, tools.URL) 179 resp, err := utils.GetValidatingHTTPClient().Get(tools.URL) 180 if err != nil { 181 return err 182 } 183 defer resp.Body.Close() 184 // Verify SHA-256 hash. 185 var buf bytes.Buffer 186 sha256, size, err := utils.ReadSHA256(io.TeeReader(resp.Body, &buf)) 187 if err != nil { 188 return err 189 } 190 if tools.SHA256 == "" { 191 logger.Errorf("no SHA-256 hash for %v", tools.SHA256) // TODO(dfc) can you spot the bug ? 192 } else if sha256 != tools.SHA256 { 193 return errors.Errorf("SHA-256 hash mismatch (%v/%v)", sha256, tools.SHA256) 194 } 195 sizeInKB := (size + 512) / 1024 196 logger.Infof("uploading %v (%dkB) to model", toolsName, sizeInKB) 197 return u.UploadTools(toolsDir, stream, tools, buf.Bytes()) 198 } 199 200 // UploadFunc is the type of Upload, which may be 201 // reassigned to control the behaviour of tools 202 // uploading. 203 type UploadFunc func(stor storage.Storage, stream string, forceVersion *version.Number, series ...string) (*coretools.Tools, error) 204 205 // Exported for testing. 206 var Upload UploadFunc = upload 207 208 // upload builds whatever version of github.com/juju/juju is in $GOPATH, 209 // uploads it to the given storage, and returns a Tools instance describing 210 // them. If forceVersion is not nil, the uploaded tools bundle will report 211 // the given version number; if any fakeSeries are supplied, additional copies 212 // of the built tools will be uploaded for use by machines of those series. 213 // Juju tools built for one series do not necessarily run on another, but this 214 // func exists only for development use cases. 215 func upload(stor storage.Storage, stream string, forceVersion *version.Number, fakeSeries ...string) (*coretools.Tools, error) { 216 builtTools, err := BuildAgentTarball(true, forceVersion, stream) 217 if err != nil { 218 return nil, err 219 } 220 defer os.RemoveAll(builtTools.Dir) 221 logger.Debugf("Uploading agent binaries for %v", fakeSeries) 222 return syncBuiltTools(stor, stream, builtTools, fakeSeries...) 223 } 224 225 // cloneToolsForSeries copies the built tools tarball into a tarball for the specified 226 // stream and series and generates corresponding metadata. 227 func cloneToolsForSeries(toolsInfo *BuiltAgent, stream string, series ...string) error { 228 // Copy the tools to the target storage, recording a Tools struct for each one. 229 var targetTools coretools.List 230 targetTools = append(targetTools, &coretools.Tools{ 231 Version: toolsInfo.Version, 232 Size: toolsInfo.Size, 233 SHA256: toolsInfo.Sha256Hash, 234 }) 235 putTools := func(vers version.Binary) (string, error) { 236 name := envtools.StorageName(vers, stream) 237 src := filepath.Join(toolsInfo.Dir, toolsInfo.StorageName) 238 dest := filepath.Join(toolsInfo.Dir, name) 239 destDir := filepath.Dir(dest) 240 if err := os.MkdirAll(destDir, 0755); err != nil { 241 return "", err 242 } 243 if err := utils.CopyFile(dest, src); err != nil { 244 return "", err 245 } 246 // Append to targetTools the attributes required to write out tools metadata. 247 targetTools = append(targetTools, &coretools.Tools{ 248 Version: vers, 249 Size: toolsInfo.Size, 250 SHA256: toolsInfo.Sha256Hash, 251 }) 252 return name, nil 253 } 254 logger.Debugf("generating tarballs for %v", series) 255 for _, series := range series { 256 _, err := jujuseries.SeriesVersion(series) 257 if err != nil { 258 return err 259 } 260 if series != toolsInfo.Version.Series { 261 fakeVersion := toolsInfo.Version 262 fakeVersion.Series = series 263 if _, err := putTools(fakeVersion); err != nil { 264 return err 265 } 266 } 267 } 268 // The tools have been copied to a temp location from which they will be uploaded, 269 // now write out the matching simplestreams metadata so that SyncTools can find them. 270 metadataStore, err := filestorage.NewFileStorageWriter(toolsInfo.Dir) 271 if err != nil { 272 return err 273 } 274 logger.Debugf("generating agent metadata") 275 return envtools.MergeAndWriteMetadata(metadataStore, stream, stream, targetTools, false) 276 } 277 278 // BuiltAgent contains metadata for a tools tarball resulting from 279 // a call to BundleTools. 280 type BuiltAgent struct { 281 Version version.Binary 282 Official bool 283 Dir string 284 StorageName string 285 Sha256Hash string 286 Size int64 287 } 288 289 // BuildAgentTarballFunc is a function which can build an agent tarball. 290 type BuildAgentTarballFunc func(build bool, forceVersion *version.Number, stream string) (*BuiltAgent, error) 291 292 // Override for testing. 293 var BuildAgentTarball BuildAgentTarballFunc = buildAgentTarball 294 295 // BuildAgentTarball bundles an agent tarball and places it in a temp directory in 296 // the expected agent path. 297 func buildAgentTarball(build bool, forceVersion *version.Number, stream string) (_ *BuiltAgent, 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("Making agent binary tarball") 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, official, sha256Hash, err := envtools.BundleTools(build, f, forceVersion) 312 if err != nil { 313 return nil, err 314 } 315 // Built agent version needs to match the client used to bootstrap. 316 builtVersion := toolsVersion 317 builtVersion.Build = 0 318 clientVersion := jujuversion.Current 319 clientVersion.Build = 0 320 if builtVersion.Number.Compare(clientVersion) != 0 { 321 return nil, errors.Errorf("agent binary %v not compatible with bootstrap client %v", toolsVersion.Number, jujuversion.Current) 322 } 323 fileInfo, err := f.Stat() 324 if err != nil { 325 return nil, errors.Errorf("cannot stat newly made agent binary archive: %v", err) 326 } 327 size := fileInfo.Size() 328 reportedVersion := toolsVersion 329 if !official && forceVersion != nil { 330 reportedVersion.Number = *forceVersion 331 } 332 if official { 333 logger.Infof("using official agent binary %v (%dkB)", toolsVersion, (size+512)/1024) 334 } else { 335 logger.Infof("using agent binary %v aliased to %v (%dkB)", toolsVersion, reportedVersion, (size+512)/1024) 336 } 337 baseToolsDir, err := ioutil.TempDir("", "juju-tools") 338 if err != nil { 339 return nil, err 340 } 341 342 // If we exit with an error, clean up the built tools directory. 343 defer func() { 344 if err != nil { 345 os.RemoveAll(baseToolsDir) 346 } 347 }() 348 349 err = os.MkdirAll(filepath.Join(baseToolsDir, storage.BaseToolsPath, stream), 0755) 350 if err != nil { 351 return nil, err 352 } 353 storageName := envtools.StorageName(toolsVersion, stream) 354 err = utils.CopyFile(filepath.Join(baseToolsDir, storageName), f.Name()) 355 if err != nil { 356 return nil, err 357 } 358 return &BuiltAgent{ 359 Version: toolsVersion, 360 Official: official, 361 Dir: baseToolsDir, 362 StorageName: storageName, 363 Size: size, 364 Sha256Hash: sha256Hash, 365 }, nil 366 } 367 368 // syncBuiltTools copies to storage a tools tarball and cloned copies for each series. 369 func syncBuiltTools(stor storage.Storage, stream string, builtTools *BuiltAgent, fakeSeries ...string) (*coretools.Tools, error) { 370 if err := cloneToolsForSeries(builtTools, stream, fakeSeries...); err != nil { 371 return nil, err 372 } 373 syncContext := &SyncContext{ 374 Source: builtTools.Dir, 375 TargetToolsFinder: StorageToolsFinder{stor}, 376 TargetToolsUploader: StorageToolsUploader{stor, false, false}, 377 AllVersions: true, 378 Stream: stream, 379 MajorVersion: builtTools.Version.Major, 380 MinorVersion: -1, 381 } 382 logger.Debugf("uploading agent binaries to cloud storage") 383 err := SyncTools(syncContext) 384 if err != nil { 385 return nil, err 386 } 387 url, err := stor.URL(builtTools.StorageName) 388 if err != nil { 389 return nil, err 390 } 391 return &coretools.Tools{ 392 Version: builtTools.Version, 393 URL: url, 394 Size: builtTools.Size, 395 SHA256: builtTools.Sha256Hash, 396 }, nil 397 } 398 399 // StorageToolsFinder is an implementation of ToolsFinder 400 // that searches for tools in the specified storage. 401 type StorageToolsFinder struct { 402 Storage storage.StorageReader 403 } 404 405 func (f StorageToolsFinder) FindTools(major int, stream string) (coretools.List, error) { 406 return envtools.ReadList(f.Storage, stream, major, -1) 407 } 408 409 // StorageToolsUplader is an implementation of ToolsUploader that 410 // writes tools to the provided storage and then writes merged 411 // metadata, optionally with mirrors. 412 type StorageToolsUploader struct { 413 Storage storage.Storage 414 WriteMetadata bool 415 WriteMirrors envtools.ShouldWriteMirrors 416 } 417 418 func (u StorageToolsUploader) UploadTools(toolsDir, stream string, tools *coretools.Tools, data []byte) error { 419 toolsName := envtools.StorageName(tools.Version, toolsDir) 420 if err := u.Storage.Put(toolsName, bytes.NewReader(data), int64(len(data))); err != nil { 421 return err 422 } 423 if !u.WriteMetadata { 424 return nil 425 } 426 err := envtools.MergeAndWriteMetadata(u.Storage, toolsDir, stream, coretools.List{tools}, u.WriteMirrors) 427 if err != nil { 428 logger.Errorf("error writing agent metadata: %v", err) 429 return err 430 } 431 return nil 432 }