OLD | NEW |
1 // Copyright 2013 Canonical Ltd. | 1 // Copyright 2013 Canonical Ltd. |
2 // Licensed under the AGPLv3, see LICENCE file for details. | 2 // Licensed under the AGPLv3, see LICENCE file for details. |
3 | 3 |
4 // The tools package supports locating, parsing, and filtering Ubuntu tools meta
data in simplestreams format. | 4 // The tools package supports locating, parsing, and filtering Ubuntu tools meta
data in simplestreams format. |
5 // See http://launchpad.net/simplestreams and in particular the doc/README file
in that project for more information | 5 // See http://launchpad.net/simplestreams and in particular the doc/README file
in that project for more information |
6 // about the file formats. | 6 // about the file formats. |
7 package tools | 7 package tools |
8 | 8 |
9 import ( | 9 import ( |
10 "bytes" | 10 "bytes" |
11 "crypto/sha256" | 11 "crypto/sha256" |
12 "fmt" | 12 "fmt" |
13 "hash" | 13 "hash" |
14 "io" | 14 "io" |
15 "net/http" | |
16 "strings" | 15 "strings" |
17 "time" | 16 "time" |
18 | 17 |
19 "launchpad.net/juju-core/environs/simplestreams" | 18 "launchpad.net/juju-core/environs/simplestreams" |
20 "launchpad.net/juju-core/environs/storage" | 19 "launchpad.net/juju-core/environs/storage" |
21 "launchpad.net/juju-core/errors" | 20 "launchpad.net/juju-core/errors" |
22 coretools "launchpad.net/juju-core/tools" | 21 coretools "launchpad.net/juju-core/tools" |
23 "launchpad.net/juju-core/utils/set" | 22 "launchpad.net/juju-core/utils/set" |
24 "launchpad.net/juju-core/version" | 23 "launchpad.net/juju-core/version" |
25 ) | 24 ) |
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
74 return allIds, nil | 73 return allIds, nil |
75 } | 74 } |
76 | 75 |
77 // ToolsMetadata holds information about a particular tools tarball. | 76 // ToolsMetadata holds information about a particular tools tarball. |
78 type ToolsMetadata struct { | 77 type ToolsMetadata struct { |
79 Release string `json:"release"` | 78 Release string `json:"release"` |
80 Version string `json:"version"` | 79 Version string `json:"version"` |
81 Arch string `json:"arch"` | 80 Arch string `json:"arch"` |
82 Size int64 `json:"size"` | 81 Size int64 `json:"size"` |
83 Path string `json:"path"` | 82 Path string `json:"path"` |
84 » FullPath string `json:"-,omitempty"` | 83 » FullPath string `json:"-"` |
85 FileType string `json:"ftype"` | 84 FileType string `json:"ftype"` |
86 SHA256 string `json:"sha256"` | 85 SHA256 string `json:"sha256"` |
87 } | 86 } |
88 | 87 |
89 func (t *ToolsMetadata) String() string { | 88 func (t *ToolsMetadata) String() string { |
90 return fmt.Sprintf("%+v", *t) | 89 return fmt.Sprintf("%+v", *t) |
91 } | 90 } |
92 | 91 |
| 92 // binary returns the tools metadata's binary version, |
| 93 // which may be used for map lookup. |
| 94 func (t *ToolsMetadata) binary() version.Binary { |
| 95 return version.Binary{ |
| 96 Number: version.MustParse(t.Version), |
| 97 Series: t.Release, |
| 98 Arch: t.Arch, |
| 99 } |
| 100 } |
| 101 |
93 func (t *ToolsMetadata) productId() (string, error) { | 102 func (t *ToolsMetadata) productId() (string, error) { |
94 seriesVersion, err := simplestreams.SeriesVersion(t.Release) | 103 seriesVersion, err := simplestreams.SeriesVersion(t.Release) |
95 if err != nil { | 104 if err != nil { |
96 return "", err | 105 return "", err |
97 } | 106 } |
98 return fmt.Sprintf("com.ubuntu.juju:%s:%s", seriesVersion, t.Arch), nil | 107 return fmt.Sprintf("com.ubuntu.juju:%s:%s", seriesVersion, t.Arch), nil |
99 } | 108 } |
100 | 109 |
101 func excludeDefaultSource(sources []simplestreams.DataSource) []simplestreams.Da
taSource { | 110 func excludeDefaultSource(sources []simplestreams.DataSource) []simplestreams.Da
taSource { |
102 var result []simplestreams.DataSource | 111 var result []simplestreams.DataSource |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
134 metadata[i] = md.(*ToolsMetadata) | 143 metadata[i] = md.(*ToolsMetadata) |
135 } | 144 } |
136 return metadata, nil | 145 return metadata, nil |
137 } | 146 } |
138 | 147 |
139 // appendMatchingTools updates matchingTools with tools metadata records from to
ols which belong to the | 148 // appendMatchingTools updates matchingTools with tools metadata records from to
ols which belong to the |
140 // specified series. If a tools record already exists in matchingTools, it is no
t overwritten. | 149 // specified series. If a tools record already exists in matchingTools, it is no
t overwritten. |
141 func appendMatchingTools(source simplestreams.DataSource, matchingTools []interf
ace{}, | 150 func appendMatchingTools(source simplestreams.DataSource, matchingTools []interf
ace{}, |
142 tools map[string]interface{}, cons simplestreams.LookupConstraint) []int
erface{} { | 151 tools map[string]interface{}, cons simplestreams.LookupConstraint) []int
erface{} { |
143 | 152 |
144 » toolsMap := make(map[string]*ToolsMetadata, len(matchingTools)) | 153 » toolsMap := make(map[version.Binary]*ToolsMetadata, len(matchingTools)) |
145 for _, val := range matchingTools { | 154 for _, val := range matchingTools { |
146 tm := val.(*ToolsMetadata) | 155 tm := val.(*ToolsMetadata) |
147 » » toolsMap[fmt.Sprintf("%s-%s-%s", tm.Release, tm.Version, tm.Arch
)] = tm | 156 » » toolsMap[tm.binary()] = tm |
148 } | 157 } |
149 for _, val := range tools { | 158 for _, val := range tools { |
150 tm := val.(*ToolsMetadata) | 159 tm := val.(*ToolsMetadata) |
151 if !set.NewStrings(cons.Params().Series...).Contains(tm.Release)
{ | 160 if !set.NewStrings(cons.Params().Series...).Contains(tm.Release)
{ |
152 continue | 161 continue |
153 } | 162 } |
154 if toolsConstraint, ok := cons.(*ToolsConstraint); ok { | 163 if toolsConstraint, ok := cons.(*ToolsConstraint); ok { |
155 tmNumber := version.MustParse(tm.Version) | 164 tmNumber := version.MustParse(tm.Version) |
156 if toolsConstraint.Version == version.Zero { | 165 if toolsConstraint.Version == version.Zero { |
157 if toolsConstraint.Released && tmNumber.IsDev()
{ | 166 if toolsConstraint.Released && tmNumber.IsDev()
{ |
158 continue | 167 continue |
159 } | 168 } |
160 if toolsConstraint.MajorVersion >= 0 && toolsCon
straint.MajorVersion != tmNumber.Major { | 169 if toolsConstraint.MajorVersion >= 0 && toolsCon
straint.MajorVersion != tmNumber.Major { |
161 continue | 170 continue |
162 } | 171 } |
163 if toolsConstraint.MinorVersion >= 0 && toolsCon
straint.MinorVersion != tmNumber.Minor { | 172 if toolsConstraint.MinorVersion >= 0 && toolsCon
straint.MinorVersion != tmNumber.Minor { |
164 continue | 173 continue |
165 } | 174 } |
166 } else { | 175 } else { |
167 if toolsConstraint.Version != tmNumber { | 176 if toolsConstraint.Version != tmNumber { |
168 continue | 177 continue |
169 } | 178 } |
170 } | 179 } |
171 } | 180 } |
172 » » if _, ok := toolsMap[fmt.Sprintf("%s-%s-%s", tm.Release, tm.Vers
ion, tm.Arch)]; !ok { | 181 » » if _, ok := toolsMap[tm.binary()]; !ok { |
173 tm.FullPath, _ = source.URL(tm.Path) | 182 tm.FullPath, _ = source.URL(tm.Path) |
174 matchingTools = append(matchingTools, tm) | 183 matchingTools = append(matchingTools, tm) |
175 } | 184 } |
176 } | 185 } |
177 return matchingTools | 186 return matchingTools |
178 } | 187 } |
179 | 188 |
180 type MetadataFile struct { | 189 type MetadataFile struct { |
181 Path string | 190 Path string |
182 Data []byte | 191 Data []byte |
183 } | 192 } |
184 | 193 |
185 func WriteMetadata(toolsList coretools.List, fetch bool, metadataStore storage.S
torage) error { | 194 // MetadataFromTools returns a tools metadata list derived from the |
186 » // Read any existing metadata so we can merge the new tools metadata wit
h what's there. | 195 // given tools list. The size and sha256 will not be computed if |
187 » // The metadata from toolsList is already present, the existing data is
overwritten. | 196 // missing. |
188 » dataSource := storage.NewStorageSimpleStreamsDataSource(metadataStore, "
tools") | 197 func MetadataFromTools(toolsList coretools.List) []*ToolsMetadata { |
| 198 » metadata := make([]*ToolsMetadata, len(toolsList)) |
| 199 » for i, t := range toolsList { |
| 200 » » path := fmt.Sprintf("releases/juju-%s-%s-%s.tgz", t.Version.Numb
er, t.Version.Series, t.Version.Arch) |
| 201 » » metadata[i] = &ToolsMetadata{ |
| 202 » » » Release: t.Version.Series, |
| 203 » » » Version: t.Version.Number.String(), |
| 204 » » » Arch: t.Version.Arch, |
| 205 » » » FullPath: t.URL, |
| 206 » » » Path: path, |
| 207 » » » FileType: "tar.gz", |
| 208 » » » Size: t.Size, |
| 209 » » » SHA256: t.SHA256, |
| 210 » » } |
| 211 » } |
| 212 » return metadata |
| 213 } |
| 214 |
| 215 // ResolveMetadata resolves incomplete metadata |
| 216 // by fetching the tools from storage and computing |
| 217 // the size and hash locally. |
| 218 func ResolveMetadata(stor storage.StorageReader, metadata []*ToolsMetadata) erro
r { |
| 219 » for _, md := range metadata { |
| 220 » » if md.Size != 0 { |
| 221 » » » continue |
| 222 » » } |
| 223 » » binary := md.binary() |
| 224 » » logger.Infof("Fetching tools to generate hash: %v", binary) |
| 225 » » var sha256hash hash.Hash |
| 226 » » size, sha256hash, err := fetchToolsHash(stor, binary) |
| 227 » » if err != nil { |
| 228 » » » return err |
| 229 » » } |
| 230 » » md.Size = size |
| 231 » » md.SHA256 = fmt.Sprintf("%x", sha256hash.Sum(nil)) |
| 232 » } |
| 233 » return nil |
| 234 } |
| 235 |
| 236 // MergeMetadata merges the given tools metadata. |
| 237 // If metadata for the same tools version exists in both lists, |
| 238 // an entry with non-empty size/SHA256 takes precedence; if |
| 239 // the two entries have different sizes/hashes, then an error is |
| 240 // returned. |
| 241 func MergeMetadata(tmlist1, tmlist2 []*ToolsMetadata) ([]*ToolsMetadata, error)
{ |
| 242 » merged := make(map[version.Binary]*ToolsMetadata) |
| 243 » for _, tm := range tmlist1 { |
| 244 » » merged[tm.binary()] = tm |
| 245 » } |
| 246 » for _, tm := range tmlist2 { |
| 247 » » binary := tm.binary() |
| 248 » » if existing, ok := merged[binary]; ok { |
| 249 » » » if tm.Size != 0 { |
| 250 » » » » if existing.Size == 0 { |
| 251 » » » » » merged[binary] = tm |
| 252 » » » » } else if existing.Size != tm.Size || existing.S
HA256 != tm.SHA256 { |
| 253 » » » » » return nil, fmt.Errorf( |
| 254 » » » » » » "metadata mismatch for %s: sizes
=(%v,%v) sha256=(%v,%v)", |
| 255 » » » » » » binary.String(), |
| 256 » » » » » » existing.Size, tm.Size, |
| 257 » » » » » » existing.SHA256, tm.SHA256, |
| 258 » » » » » ) |
| 259 » » » » } |
| 260 » » » } |
| 261 » » } else { |
| 262 » » » merged[binary] = tm |
| 263 » » } |
| 264 » } |
| 265 » list := make([]*ToolsMetadata, 0, len(merged)) |
| 266 » for _, metadata := range merged { |
| 267 » » list = append(list, metadata) |
| 268 » } |
| 269 » return list, nil |
| 270 } |
| 271 |
| 272 // ReadMetadata returns the tools metadata from the given storage. |
| 273 func ReadMetadata(store storage.StorageReader) ([]*ToolsMetadata, error) { |
| 274 » dataSource := storage.NewStorageSimpleStreamsDataSource(store, "tools") |
189 toolsConstraint, err := makeToolsConstraint(simplestreams.CloudSpec{}, -
1, -1, coretools.Filter{}) | 275 toolsConstraint, err := makeToolsConstraint(simplestreams.CloudSpec{}, -
1, -1, coretools.Filter{}) |
190 if err != nil { | 276 if err != nil { |
| 277 return nil, err |
| 278 } |
| 279 metadata, err := Fetch([]simplestreams.DataSource{dataSource}, simplestr
eams.DefaultIndexPath, toolsConstraint, false) |
| 280 if err != nil && !errors.IsNotFoundError(err) { |
| 281 return nil, err |
| 282 } |
| 283 return metadata, nil |
| 284 } |
| 285 |
| 286 // WriteMetadata writes the given tools metadata to the given storage. |
| 287 func WriteMetadata(stor storage.Storage, metadata []*ToolsMetadata) error { |
| 288 index, products, err := MarshalToolsMetadataJSON(metadata, time.Now()) |
| 289 if err != nil { |
191 return err | 290 return err |
192 } | 291 } |
193 » existingMetadata, err := Fetch([]simplestreams.DataSource{dataSource}, s
implestreams.DefaultIndexPath, toolsConstraint, false) | 292 » metadataInfo := []MetadataFile{ |
194 » if err != nil && !errors.IsNotFoundError(err) { | 293 » » {simplestreams.UnsignedIndex, index}, |
195 » » return err | 294 » » {ProductMetadataPath, products}, |
196 » } | |
197 » newToolsVersions := make(map[string]bool) | |
198 » for _, tool := range toolsList { | |
199 » » newToolsVersions[tool.Version.String()] = true | |
200 » } | |
201 » // Merge in existing records. | |
202 » for _, toolsMetadata := range existingMetadata { | |
203 » » vers := version.Binary{version.MustParse(toolsMetadata.Version),
toolsMetadata.Release, toolsMetadata.Arch} | |
204 » » if _, ok := newToolsVersions[vers.String()]; ok { | |
205 » » » continue | |
206 » » } | |
207 » » tool := &coretools.Tools{ | |
208 » » » Version: vers, | |
209 » » » SHA256: toolsMetadata.SHA256, | |
210 » » » Size: toolsMetadata.Size, | |
211 » » } | |
212 » » toolsList = append(toolsList, tool) | |
213 » } | |
214 » metadataInfo, err := generateMetadata(toolsList, fetch) | |
215 » if err != nil { | |
216 » » return err | |
217 } | 295 } |
218 for _, md := range metadataInfo { | 296 for _, md := range metadataInfo { |
219 logger.Infof("Writing %s", "tools/"+md.Path) | 297 logger.Infof("Writing %s", "tools/"+md.Path) |
220 » » err = metadataStore.Put("tools/"+md.Path, bytes.NewReader(md.Dat
a), int64(len(md.Data))) | 298 » » err = stor.Put("tools/"+md.Path, bytes.NewReader(md.Data), int64
(len(md.Data))) |
221 if err != nil { | 299 if err != nil { |
222 return err | 300 return err |
223 } | 301 } |
224 } | 302 } |
225 return nil | 303 return nil |
226 } | 304 } |
227 | 305 |
228 func generateMetadata(toolsList coretools.List, fetch bool) ([]MetadataFile, err
or) { | 306 // MergeAndWriteMetadata reads the existing metadata from storage (if any), |
229 » metadata := make([]*ToolsMetadata, len(toolsList)) | 307 // and merges it with metadata generated from the given tools list. The |
230 » for i, t := range toolsList { | 308 // resulting metadata is written to storage. |
231 » » var size int64 | 309 func MergeAndWriteMetadata(stor storage.Storage, tools coretools.List) error { |
232 » » var sha256hex string | 310 » existing, err := ReadMetadata(stor) |
233 » » var err error | 311 » if err != nil { |
234 » » if fetch && t.Size == 0 { | 312 » » return err |
235 » » » logger.Infof("Fetching tools to generate hash: %v", t.UR
L) | |
236 » » » var sha256hash hash.Hash | |
237 » » » size, sha256hash, err = fetchToolsHash(t.URL) | |
238 » » » if err != nil { | |
239 » » » » return nil, err | |
240 » » » } | |
241 » » » sha256hex = fmt.Sprintf("%x", sha256hash.Sum(nil)) | |
242 » » } else { | |
243 » » » size = t.Size | |
244 » » » sha256hex = t.SHA256 | |
245 » » } | |
246 | |
247 » » path := fmt.Sprintf("releases/juju-%s-%s-%s.tgz", t.Version.Numb
er, t.Version.Series, t.Version.Arch) | |
248 » » metadata[i] = &ToolsMetadata{ | |
249 » » » Release: t.Version.Series, | |
250 » » » Version: t.Version.Number.String(), | |
251 » » » Arch: t.Version.Arch, | |
252 » » » Path: path, | |
253 » » » FileType: "tar.gz", | |
254 » » » Size: size, | |
255 » » » SHA256: sha256hex, | |
256 » » } | |
257 } | 313 } |
258 | 314 » metadata := MetadataFromTools(tools) |
259 » index, products, err := MarshalToolsMetadataJSON(metadata, time.Now()) | 315 » if metadata, err = MergeMetadata(metadata, existing); err != nil { |
260 » if err != nil { | 316 » » return err |
261 » » return nil, err | |
262 } | 317 } |
263 » objects := []MetadataFile{ | 318 » return WriteMetadata(stor, metadata) |
264 » » {simplestreams.UnsignedIndex, index}, | |
265 » » {ProductMetadataPath, products}, | |
266 » } | |
267 » return objects, nil | |
268 } | 319 } |
269 | 320 |
270 // fetchToolsHash fetches the file at the specified URL, | 321 // fetchToolsHash fetches the tools from storage and calculates |
271 // and calculates its size in bytes and computes a SHA256 | 322 // its size in bytes and computes a SHA256 hash of its contents. |
272 // hash of its contents. | 323 func fetchToolsHash(stor storage.StorageReader, ver version.Binary) (size int64,
sha256hash hash.Hash, err error) { |
273 func fetchToolsHash(url string) (size int64, sha256hash hash.Hash, err error) { | 324 » r, err := storage.Get(stor, StorageName(ver)) |
274 » resp, err := http.Get(url) | |
275 if err != nil { | 325 if err != nil { |
276 return 0, nil, err | 326 return 0, nil, err |
277 } | 327 } |
| 328 defer r.Close() |
278 sha256hash = sha256.New() | 329 sha256hash = sha256.New() |
279 » size, err = io.Copy(sha256hash, resp.Body) | 330 » size, err = io.Copy(sha256hash, r) |
280 » resp.Body.Close() | |
281 return size, sha256hash, err | 331 return size, sha256hash, err |
282 } | 332 } |
OLD | NEW |