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