github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/environs/tools/simplestreams.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 // The tools package supports locating, parsing, and filtering Ubuntu tools metadata in simplestreams format. 5 // See http://launchpad.net/simplestreams and in particular the doc/README file in that project for more information 6 // about the file formats. 7 package tools 8 9 import ( 10 "bytes" 11 "crypto/sha256" 12 "encoding/json" 13 "fmt" 14 "hash" 15 "io" 16 "io/ioutil" 17 "path" 18 "sort" 19 "strings" 20 "time" 21 22 "github.com/juju/collections/set" 23 "github.com/juju/errors" 24 "github.com/juju/os/series" 25 "github.com/juju/utils/arch" 26 "github.com/juju/version" 27 28 "github.com/juju/juju/environs/simplestreams" 29 "github.com/juju/juju/environs/storage" 30 coretools "github.com/juju/juju/tools" 31 ) 32 33 func init() { 34 simplestreams.RegisterStructTags(ToolsMetadata{}) 35 } 36 37 const ( 38 // ImageIds is the simplestreams tools content type. 39 ContentDownload = "content-download" 40 41 // StreamsVersionV1 is used to construct the path for accessing streams data. 42 StreamsVersionV1 = "v1" 43 44 // IndexFileVersion is used to construct the streams index file. 45 IndexFileVersion = 2 46 ) 47 48 var currentStreamsVersion = StreamsVersionV1 49 50 // This needs to be a var so we can override it for testing. 51 var DefaultBaseURL = "https://streams.canonical.com/juju/tools" 52 53 const ( 54 // Used to specify the released tools metadata. 55 ReleasedStream = "released" 56 57 // Used to specify metadata for testing tools. 58 TestingStream = "testing" 59 60 // Used to specify the proposed tools metadata. 61 ProposedStream = "proposed" 62 63 // Used to specify the devel tools metadata. 64 DevelStream = "devel" 65 ) 66 67 // ToolsConstraint defines criteria used to find a tools metadata record. 68 type ToolsConstraint struct { 69 simplestreams.LookupParams 70 Version version.Number 71 MajorVersion int 72 MinorVersion int 73 } 74 75 // NewVersionedToolsConstraint returns a ToolsConstraint for a tools with a specific version. 76 func NewVersionedToolsConstraint(vers version.Number, params simplestreams.LookupParams) *ToolsConstraint { 77 return &ToolsConstraint{LookupParams: params, Version: vers} 78 } 79 80 // NewGeneralToolsConstraint returns a ToolsConstraint for tools with matching major/minor version numbers. 81 func NewGeneralToolsConstraint(majorVersion, minorVersion int, params simplestreams.LookupParams) *ToolsConstraint { 82 return &ToolsConstraint{LookupParams: params, Version: version.Zero, 83 MajorVersion: majorVersion, MinorVersion: minorVersion} 84 } 85 86 // IndexIds generates a string array representing product ids formed similarly to an ISCSI qualified name (IQN). 87 func (tc *ToolsConstraint) IndexIds() []string { 88 if tc.Stream == "" { 89 return nil 90 } 91 return []string{ToolsContentId(tc.Stream)} 92 } 93 94 // ProductIds generates a string array representing product ids formed similarly to an ISCSI qualified name (IQN). 95 func (tc *ToolsConstraint) ProductIds() ([]string, error) { 96 var allIds []string 97 for _, ser := range tc.Series { 98 version, err := series.SeriesVersion(ser) 99 if err != nil { 100 if series.IsUnknownSeriesVersionError(err) { 101 logger.Debugf("ignoring unknown series %q", ser) 102 continue 103 } 104 return nil, err 105 } 106 ids := make([]string, len(tc.Arches)) 107 for i, arch := range tc.Arches { 108 ids[i] = fmt.Sprintf("com.ubuntu.juju:%s:%s", version, arch) 109 } 110 allIds = append(allIds, ids...) 111 } 112 return allIds, nil 113 } 114 115 // ToolsMetadata holds information about a particular tools tarball. 116 type ToolsMetadata struct { 117 Release string `json:"release"` 118 Version string `json:"version"` 119 Arch string `json:"arch"` 120 Size int64 `json:"size"` 121 Path string `json:"path"` 122 FullPath string `json:"-"` 123 FileType string `json:"ftype"` 124 SHA256 string `json:"sha256"` 125 } 126 127 func (t *ToolsMetadata) String() string { 128 return fmt.Sprintf("%+v", *t) 129 } 130 131 // sortString is used by byVersion to sort a list of ToolsMetadata. 132 func (t *ToolsMetadata) sortString() string { 133 return fmt.Sprintf("%v-%s-%s", t.Version, t.Release, t.Arch) 134 } 135 136 // binary returns the tools metadata's binary version, which may be used for 137 // map lookup. 138 func (t *ToolsMetadata) binary() (version.Binary, error) { 139 num, err := version.Parse(t.Version) 140 if err != nil { 141 return version.Binary{}, errors.Trace(err) 142 } 143 return version.Binary{ 144 Number: num, 145 Series: t.Release, 146 Arch: t.Arch, 147 }, nil 148 } 149 150 func (t *ToolsMetadata) productId() (string, error) { 151 seriesVersion, err := series.SeriesVersion(t.Release) 152 if err != nil { 153 return "", err 154 } 155 return fmt.Sprintf("com.ubuntu.juju:%s:%s", seriesVersion, t.Arch), nil 156 } 157 158 // Fetch returns a list of tools for the specified cloud matching the constraint. 159 // The base URL locations are as specified - the first location which has a file is the one used. 160 // Signed data is preferred, but if there is no signed data available and onlySigned is false, 161 // then unsigned data is used. 162 func Fetch( 163 sources []simplestreams.DataSource, cons *ToolsConstraint, 164 ) ([]*ToolsMetadata, *simplestreams.ResolveInfo, error) { 165 166 params := simplestreams.GetMetadataParams{ 167 StreamsVersion: currentStreamsVersion, 168 LookupConstraint: cons, 169 ValueParams: simplestreams.ValueParams{ 170 DataType: ContentDownload, 171 FilterFunc: appendMatchingTools, 172 MirrorContentId: ToolsContentId(cons.Stream), 173 ValueTemplate: ToolsMetadata{}, 174 }, 175 } 176 items, resolveInfo, err := simplestreams.GetMetadata(sources, params) 177 if err != nil { 178 return nil, nil, err 179 } 180 metadata := make([]*ToolsMetadata, len(items)) 181 for i, md := range items { 182 metadata[i] = md.(*ToolsMetadata) 183 } 184 // Sorting the metadata is not strictly necessary, but it ensures consistent ordering for 185 // all compilers, and it just makes it easier to look at the data. 186 Sort(metadata) 187 return metadata, resolveInfo, nil 188 } 189 190 // Sort sorts a slice of ToolsMetadata in ascending order of their version 191 // in order to ensure the results of Fetch are ordered deterministically. 192 func Sort(metadata []*ToolsMetadata) { 193 sort.Sort(byVersion(metadata)) 194 } 195 196 type byVersion []*ToolsMetadata 197 198 func (b byVersion) Len() int { return len(b) } 199 func (b byVersion) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 200 func (b byVersion) Less(i, j int) bool { return b[i].sortString() < b[j].sortString() } 201 202 // appendMatchingTools updates matchingTools with tools metadata records from tools which belong to the 203 // specified series. If a tools record already exists in matchingTools, it is not overwritten. 204 func appendMatchingTools(source simplestreams.DataSource, matchingTools []interface{}, 205 tools map[string]interface{}, cons simplestreams.LookupConstraint) ([]interface{}, error) { 206 207 toolsMap := make(map[version.Binary]*ToolsMetadata, len(matchingTools)) 208 for _, val := range matchingTools { 209 tm := val.(*ToolsMetadata) 210 binary, err := tm.binary() 211 if err != nil { 212 return nil, errors.Trace(err) 213 } 214 toolsMap[binary] = tm 215 } 216 for _, val := range tools { 217 tm := val.(*ToolsMetadata) 218 if !set.NewStrings(cons.Params().Series...).Contains(tm.Release) { 219 continue 220 } 221 if toolsConstraint, ok := cons.(*ToolsConstraint); ok { 222 tmNumber := version.MustParse(tm.Version) 223 if toolsConstraint.Version == version.Zero { 224 if toolsConstraint.MajorVersion >= 0 && toolsConstraint.MajorVersion != tmNumber.Major { 225 continue 226 } 227 if toolsConstraint.MinorVersion >= 0 && toolsConstraint.MinorVersion != tmNumber.Minor { 228 continue 229 } 230 } else { 231 if toolsConstraint.Version != tmNumber { 232 continue 233 } 234 } 235 } 236 binary, err := tm.binary() 237 if err != nil { 238 return nil, errors.Trace(err) 239 } 240 if _, ok := toolsMap[binary]; !ok { 241 tm.FullPath, _ = source.URL(tm.Path) 242 matchingTools = append(matchingTools, tm) 243 } 244 } 245 return matchingTools, nil 246 } 247 248 type MetadataFile struct { 249 Path string 250 Data []byte 251 } 252 253 // MetadataFromTools returns a tools metadata list derived from the 254 // given tools list. The size and sha256 will not be computed if 255 // missing. 256 func MetadataFromTools(toolsList coretools.List, toolsDir string) []*ToolsMetadata { 257 metadata := make([]*ToolsMetadata, len(toolsList)) 258 for i, t := range toolsList { 259 path := fmt.Sprintf("%s/juju-%s-%s-%s.tgz", toolsDir, t.Version.Number, t.Version.Series, t.Version.Arch) 260 metadata[i] = &ToolsMetadata{ 261 Release: t.Version.Series, 262 Version: t.Version.Number.String(), 263 Arch: t.Version.Arch, 264 Path: path, 265 FileType: "tar.gz", 266 Size: t.Size, 267 SHA256: t.SHA256, 268 } 269 } 270 return metadata 271 } 272 273 // ResolveMetadata resolves incomplete metadata 274 // by fetching the tools from storage and computing 275 // the size and hash locally. 276 func ResolveMetadata(stor storage.StorageReader, toolsDir string, metadata []*ToolsMetadata) error { 277 for _, md := range metadata { 278 if md.Size != 0 { 279 continue 280 } 281 binary, err := md.binary() 282 if err != nil { 283 return errors.Annotate(err, "cannot resolve metadata") 284 } 285 logger.Infof("Fetching agent binaries from dir %q to generate hash: %v", toolsDir, binary) 286 size, sha256hash, err := fetchToolsHash(stor, toolsDir, binary) 287 // Older versions of Juju only know about ppc64, not ppc64el, 288 // so if there's no metadata for ppc64, dd metadata for that arch. 289 if errors.IsNotFound(err) && binary.Arch == arch.LEGACY_PPC64 { 290 ppc64elBinary := binary 291 ppc64elBinary.Arch = arch.PPC64EL 292 md.Path = strings.Replace(md.Path, binary.Arch, ppc64elBinary.Arch, -1) 293 size, sha256hash, err = fetchToolsHash(stor, toolsDir, ppc64elBinary) 294 } 295 if err != nil { 296 return err 297 } 298 md.Size = size 299 md.SHA256 = fmt.Sprintf("%x", sha256hash.Sum(nil)) 300 } 301 return nil 302 } 303 304 // MergeMetadata merges the given tools metadata. 305 // If metadata for the same tools version exists in both lists, 306 // an entry with non-empty size/SHA256 takes precedence; if 307 // the two entries have different sizes/hashes, then an error is 308 // returned. 309 func MergeMetadata(tmlist1, tmlist2 []*ToolsMetadata) ([]*ToolsMetadata, error) { 310 merged := make(map[version.Binary]*ToolsMetadata) 311 for _, tm := range tmlist1 { 312 binary, err := tm.binary() 313 if err != nil { 314 return nil, errors.Annotate(err, "cannot merge metadata") 315 } 316 merged[binary] = tm 317 } 318 for _, tm := range tmlist2 { 319 binary, err := tm.binary() 320 if err != nil { 321 return nil, errors.Annotate(err, "cannot merge metadata") 322 } 323 if existing, ok := merged[binary]; ok { 324 if tm.Size != 0 { 325 if existing.Size == 0 { 326 merged[binary] = tm 327 } else if existing.Size != tm.Size || existing.SHA256 != tm.SHA256 { 328 return nil, fmt.Errorf( 329 "metadata mismatch for %s: sizes=(%v,%v) sha256=(%v,%v)", 330 binary.String(), 331 existing.Size, tm.Size, 332 existing.SHA256, tm.SHA256, 333 ) 334 } 335 } 336 } else { 337 merged[binary] = tm 338 } 339 } 340 list := make([]*ToolsMetadata, 0, len(merged)) 341 for _, metadata := range merged { 342 list = append(list, metadata) 343 } 344 Sort(list) 345 return list, nil 346 } 347 348 // ReadMetadata returns the tools metadata from the given storage for the specified stream. 349 func ReadMetadata(store storage.StorageReader, stream string) ([]*ToolsMetadata, error) { 350 dataSource := storage.NewStorageSimpleStreamsDataSource("existing metadata", store, storage.BaseToolsPath, simplestreams.EXISTING_CLOUD_DATA, false) 351 toolsConstraint, err := makeToolsConstraint(simplestreams.CloudSpec{}, stream, -1, -1, coretools.Filter{}) 352 if err != nil { 353 return nil, err 354 } 355 metadata, _, err := Fetch([]simplestreams.DataSource{dataSource}, toolsConstraint) 356 if err != nil && !errors.IsNotFound(err) { 357 return nil, err 358 } 359 return metadata, nil 360 } 361 362 // AllMetadataStreams is the set of streams for which there will be simplestreams tools metadata. 363 var AllMetadataStreams = []string{ReleasedStream, ProposedStream, TestingStream, DevelStream} 364 365 // ReadAllMetadata returns the tools metadata from the given storage for all streams. 366 // The result is a map of metadata slices, keyed on stream. 367 func ReadAllMetadata(store storage.StorageReader) (map[string][]*ToolsMetadata, error) { 368 streamMetadata := make(map[string][]*ToolsMetadata) 369 for _, stream := range AllMetadataStreams { 370 metadata, err := ReadMetadata(store, stream) 371 if err != nil { 372 return nil, err 373 } 374 if len(metadata) == 0 { 375 continue 376 } 377 streamMetadata[stream] = metadata 378 } 379 return streamMetadata, nil 380 } 381 382 // removeMetadataUpdated unmarshalls simplestreams metadata, clears the 383 // updated attribute, and then marshalls back to a string. 384 func removeMetadataUpdated(metadataBytes []byte) (string, error) { 385 var metadata map[string]interface{} 386 err := json.Unmarshal(metadataBytes, &metadata) 387 if err != nil { 388 return "", err 389 } 390 delete(metadata, "updated") 391 392 metadataJson, err := json.Marshal(metadata) 393 if err != nil { 394 return "", err 395 } 396 return string(metadataJson), nil 397 } 398 399 // metadataUnchanged returns true if the content of metadata for stream in stor is the same 400 // as generatedMetadata, ignoring the "updated" attribute. 401 func metadataUnchanged(stor storage.Storage, stream string, generatedMetadata []byte) (bool, error) { 402 mdPath := ProductMetadataPath(stream) 403 filePath := path.Join(storage.BaseToolsPath, mdPath) 404 existingDataReader, err := stor.Get(filePath) 405 // If the file can't be retrieved, consider it has changed. 406 if err != nil { 407 return false, nil 408 } 409 defer existingDataReader.Close() 410 existingData, err := ioutil.ReadAll(existingDataReader) 411 if err != nil { 412 return false, err 413 } 414 415 // To do the comparison, we unmarshall the metadata, clear the 416 // updated value, and marshall back to a string. 417 existingMetadata, err := removeMetadataUpdated(existingData) 418 if err != nil { 419 return false, err 420 } 421 newMetadata, err := removeMetadataUpdated(generatedMetadata) 422 if err != nil { 423 return false, err 424 } 425 return existingMetadata == newMetadata, nil 426 } 427 428 // WriteMetadata writes the given tools metadata for the specified streams to the given storage. 429 // streamMetadata contains all known metadata so that the correct index files can be written. 430 // Only product files for the specified streams are written. 431 func WriteMetadata(stor storage.Storage, streamMetadata map[string][]*ToolsMetadata, streams []string, writeMirrors ShouldWriteMirrors) error { 432 // TODO(perrito666) 2016-05-02 lp:1558657 433 updated := time.Now() 434 index, legacyIndex, products, err := MarshalToolsMetadataJSON(streamMetadata, updated) 435 if err != nil { 436 return err 437 } 438 metadataInfo := []MetadataFile{ 439 {simplestreams.UnsignedIndex(currentStreamsVersion, IndexFileVersion), index}, 440 } 441 if legacyIndex != nil { 442 metadataInfo = append(metadataInfo, MetadataFile{ 443 simplestreams.UnsignedIndex(currentStreamsVersion, 1), legacyIndex, 444 }) 445 } 446 for _, stream := range streams { 447 if metadata, ok := products[stream]; ok { 448 // If metadata hasn't changed, do not overwrite. 449 unchanged, err := metadataUnchanged(stor, stream, metadata) 450 if err != nil { 451 return err 452 } 453 if unchanged { 454 logger.Infof("Metadata for stream %q unchanged", stream) 455 continue 456 } 457 // Metadata is different, so include it. 458 metadataInfo = append(metadataInfo, MetadataFile{ProductMetadataPath(stream), metadata}) 459 } 460 } 461 if writeMirrors { 462 streamsMirrorsMetadata := make(map[string][]simplestreams.MirrorReference) 463 for stream := range streamMetadata { 464 streamsMirrorsMetadata[ToolsContentId(stream)] = []simplestreams.MirrorReference{{ 465 Updated: updated.Format("20060102"), // YYYYMMDD 466 DataType: ContentDownload, 467 Format: simplestreams.MirrorFormat, 468 Path: simplestreams.MirrorFile, 469 }} 470 } 471 mirrorsMetadata := map[string]map[string][]simplestreams.MirrorReference{ 472 "mirrors": streamsMirrorsMetadata, 473 } 474 mirrorsInfo, err := json.MarshalIndent(&mirrorsMetadata, "", " ") 475 if err != nil { 476 return err 477 } 478 metadataInfo = append( 479 metadataInfo, MetadataFile{simplestreams.UnsignedMirror(currentStreamsVersion), mirrorsInfo}) 480 } 481 return writeMetadataFiles(stor, metadataInfo) 482 } 483 484 var writeMetadataFiles = func(stor storage.Storage, metadataInfo []MetadataFile) error { 485 for _, md := range metadataInfo { 486 filePath := path.Join(storage.BaseToolsPath, md.Path) 487 logger.Infof("Writing %s", filePath) 488 err := stor.Put(filePath, bytes.NewReader(md.Data), int64(len(md.Data))) 489 if err != nil { 490 return err 491 } 492 } 493 return nil 494 } 495 496 type ShouldWriteMirrors bool 497 498 const ( 499 WriteMirrors = ShouldWriteMirrors(true) 500 DoNotWriteMirrors = ShouldWriteMirrors(false) 501 ) 502 503 // MergeAndWriteMetadata reads the existing metadata from storage (if any), 504 // and merges it with metadata generated from the given tools list. The 505 // resulting metadata is written to storage. 506 func MergeAndWriteMetadata(stor storage.Storage, toolsDir, stream string, tools coretools.List, writeMirrors ShouldWriteMirrors) error { 507 existing, err := ReadAllMetadata(stor) 508 if err != nil { 509 return err 510 } 511 metadata := MetadataFromTools(tools, toolsDir) 512 if metadata, err = MergeMetadata(metadata, existing[stream]); err != nil { 513 return err 514 } 515 existing[stream] = metadata 516 return WriteMetadata(stor, existing, []string{stream}, writeMirrors) 517 } 518 519 // fetchToolsHash fetches the tools from storage and calculates 520 // its size in bytes and computes a SHA256 hash of its contents. 521 func fetchToolsHash(stor storage.StorageReader, stream string, ver version.Binary) (size int64, sha256hash hash.Hash, err error) { 522 r, err := storage.Get(stor, StorageName(ver, stream)) 523 if err != nil { 524 return 0, nil, err 525 } 526 defer r.Close() 527 sha256hash = sha256.New() 528 size, err = io.Copy(sha256hash, r) 529 return size, sha256hash, err 530 }