1use anyhow::{anyhow, bail, Context, Result};
16use reqwest::{blocking, StatusCode, Url};
17use serde::Deserialize;
18use std::collections::HashMap;
19use std::fmt::{Display, Formatter};
20use std::fs::OpenOptions;
21use std::io::{Read, Seek, SeekFrom};
22use std::path::{Path, PathBuf};
23use std::thread::sleep;
24use std::time::Duration;
25
26use crate::cmdline::*;
27use crate::osmet::*;
28use crate::util::set_die_on_sigpipe;
29
30const HTTP_COMPLETION_TIMEOUT: Duration = Duration::from_secs(4 * 60 * 60);
32
33const DEFAULT_STREAM_BASE_URL: &str = "https://builds.coreos.fedoraproject.org/streams/";
35
36const OSMET_FILES_DIR: &str = "/run/coreos-installer/osmet";
38
39pub trait ImageLocation: Display {
40 fn sources(&self) -> Result<Vec<ImageSource>>;
42
43 fn require_signature(&self) -> bool {
45 true
46 }
47}
48
49#[derive(Debug)]
51pub struct FileLocation {
52 image_path: String,
53 sig_path: String,
54}
55
56pub struct OsmetLocation {
58 osmet_path: PathBuf,
59 architecture: String,
60 sector_size: u32,
61 description: String,
62}
63
64#[derive(Debug)]
66pub struct UrlLocation {
67 image_url: Url,
68 sig_url: Url,
69 artifact_type: String,
70 retries: FetchRetries,
71}
72
73#[derive(Debug)]
75pub struct StreamLocation {
76 stream_base_url: Option<Url>,
77 stream: String,
78 stream_url: Url,
79 architecture: String,
80 platform: String,
81 format: String,
82 retries: FetchRetries,
83}
84
85pub struct ImageSource {
86 pub reader: Box<dyn Read>,
87 pub length_hint: Option<u64>,
88 pub signature: Option<Vec<u8>>,
89 pub filename: String,
90 pub artifact_type: String,
91}
92
93impl FileLocation {
94 pub fn new(path: &str) -> Self {
95 Self {
96 image_path: path.to_string(),
97 sig_path: format!("{path}.sig"),
98 }
99 }
100}
101
102impl Display for FileLocation {
103 fn fmt(&self, f: &mut Formatter<'_>) -> ::std::fmt::Result {
104 write!(
105 f,
106 "Copying image from {}\nReading signature from {}",
107 self.image_path, self.sig_path
108 )
109 }
110}
111
112impl ImageLocation for FileLocation {
113 fn sources(&self) -> Result<Vec<ImageSource>> {
114 let mut out = OpenOptions::new()
116 .read(true)
117 .open(&self.image_path)
118 .context("opening source image file")?;
119
120 let length = out
122 .seek(SeekFrom::End(0))
123 .context("seeking source image file")?;
124 out.rewind().context("seeking source image file")?;
125
126 let signature = match OpenOptions::new().read(true).open(&self.sig_path) {
128 Ok(mut file) => {
129 let mut sig_vec = Vec::new();
130 file.read_to_end(&mut sig_vec)
131 .context("reading signature file")?;
132 Some(sig_vec)
133 }
134 Err(err) => {
135 eprintln!("Couldn't read signature file: {err}");
136 None
137 }
138 };
139 let filename = Path::new(&self.image_path)
140 .file_name()
141 .context("extracting filename")?
142 .to_string_lossy()
143 .to_string();
144
145 Ok(vec![ImageSource {
146 reader: Box::new(out),
147 length_hint: Some(length),
148 signature,
149 filename,
150 artifact_type: "disk".to_string(),
151 }])
152 }
153}
154
155impl UrlLocation {
156 pub fn new(url: &Url, retries: FetchRetries) -> Self {
157 let mut sig_url = url.clone();
158 sig_url.set_path(&format!("{}.sig", sig_url.path()));
159 Self::new_full(url, &sig_url, "disk", retries)
160 }
161
162 fn new_full(url: &Url, sig_url: &Url, artifact_type: &str, retries: FetchRetries) -> Self {
163 Self {
164 image_url: url.clone(),
165 sig_url: sig_url.clone(),
166 artifact_type: artifact_type.to_string(),
167 retries,
168 }
169 }
170
171 fn fetch_signature(&self) -> Result<Vec<u8>> {
173 let client = new_http_client()?;
174 let mut resp =
175 http_get(client, &self.sig_url, self.retries).context("fetching signature URL")?;
176
177 let mut sig_bytes = Vec::new();
178 resp.read_to_end(&mut sig_bytes)
179 .context("reading signature content")?;
180 Ok(sig_bytes)
181 }
182}
183
184impl Display for UrlLocation {
185 fn fmt(&self, f: &mut Formatter<'_>) -> ::std::fmt::Result {
186 write!(
187 f,
188 "Downloading image from {}\nDownloading signature from {}",
189 self.image_url, self.sig_url
190 )
191 }
192}
193
194impl ImageLocation for UrlLocation {
195 fn sources(&self) -> Result<Vec<ImageSource>> {
196 let signature = self
197 .fetch_signature()
198 .map_err(|e| eprintln!("Failed to fetch signature: {e}"))
199 .ok();
200
201 let client = new_http_client()?;
203 let resp = http_get(client, &self.image_url, self.retries).context("fetching image URL")?;
204 match resp.status() {
205 StatusCode::OK => (),
206 s => bail!("image fetch failed: {}", s),
207 };
208 let length_hint = resp.content_length();
209 let filename = resp
211 .url()
212 .path_segments()
213 .context("splitting image URL")?
214 .next_back()
215 .context("walking image URL")?
216 .to_string();
217
218 Ok(vec![ImageSource {
219 reader: Box::new(resp),
220 length_hint,
221 signature,
222 filename,
223 artifact_type: self.artifact_type.clone(),
224 }])
225 }
226}
227
228impl StreamLocation {
229 pub fn new(
230 stream: &str,
231 architecture: &str,
232 platform: &str,
233 format: &str,
234 base_url: Option<&Url>,
235 retries: FetchRetries,
236 ) -> Result<Self> {
237 Ok(Self {
238 stream_base_url: base_url.cloned(),
239 stream: stream.to_string(),
240 stream_url: build_stream_url(stream, base_url)?,
241 architecture: architecture.to_string(),
242 platform: platform.to_string(),
243 format: format.to_string(),
244 retries,
245 })
246 }
247}
248
249impl Display for StreamLocation {
250 fn fmt(&self, f: &mut Formatter<'_>) -> ::std::fmt::Result {
251 if self.stream_base_url.is_some() {
252 write!(
253 f,
254 "Downloading {} {} image ({}) and signature referenced from {}",
255 self.architecture, self.platform, self.format, self.stream_url
256 )
257 } else {
258 write!(
259 f,
260 "Downloading Fedora CoreOS {} {} {} image ({}) and signature",
261 self.stream, self.architecture, self.platform, self.format
262 )
263 }
264 }
265}
266
267impl ImageLocation for StreamLocation {
268 fn sources(&self) -> Result<Vec<ImageSource>> {
269 let client = new_http_client()?;
271 let stream = fetch_stream(client, &self.stream_url, self.retries)?;
272
273 let artifacts = stream
275 .architectures
276 .get(&self.architecture)
277 .map(|arch| arch.artifacts.get(&self.platform))
278 .unwrap_or(None)
279 .map(|platform| platform.formats.get(&self.format))
280 .unwrap_or(None)
281 .with_context(|| {
282 format!(
283 "couldn't find architecture {}, platform {}, format {} in stream metadata",
284 self.architecture, self.platform, self.format
285 )
286 })?;
287
288 let mut sources: Vec<ImageSource> = Vec::new();
290 for (artifact_type, artifact) in artifacts.iter() {
291 let artifact_url = Url::parse(&artifact.location)
292 .context("parsing artifact URL from stream metadata")?;
293 let signature_url = Url::parse(&artifact.signature)
294 .context("parsing signature URL from stream metadata")?;
295 let mut artifact_sources =
296 UrlLocation::new_full(&artifact_url, &signature_url, artifact_type, self.retries)
297 .sources()?;
298 sources.append(&mut artifact_sources);
299 }
300 sources.sort_by_key(|k| k.artifact_type.to_string());
301 Ok(sources)
302 }
303}
304
305impl OsmetLocation {
306 pub fn new(architecture: &str, sector_size: u32) -> Result<Option<Self>> {
307 let osmet_dir = Path::new(OSMET_FILES_DIR);
308 if !osmet_dir.exists() {
309 return Ok(None);
310 }
311
312 if let Some((osmet_path, description)) =
313 find_matching_osmet_in_dir(osmet_dir, architecture, sector_size)?
314 {
315 Ok(Some(Self {
316 osmet_path,
317 architecture: architecture.into(),
318 sector_size,
319 description,
320 }))
321 } else {
322 Ok(None)
323 }
324 }
325}
326
327impl Display for OsmetLocation {
328 fn fmt(&self, f: &mut Formatter<'_>) -> ::std::fmt::Result {
329 write!(
330 f,
331 "Installing {} {} ({}-byte sectors)",
332 self.description, self.architecture, self.sector_size
333 )
334 }
335}
336
337impl ImageLocation for OsmetLocation {
338 fn sources(&self) -> Result<Vec<ImageSource>> {
339 let unpacker = OsmetUnpacker::new_from_sysroot(Path::new(&self.osmet_path))?;
340
341 let filename = {
342 let stem = self.osmet_path.file_stem().with_context(|| {
343 format!(
347 "can't create new .raw filename from osmet path {:?}",
348 &self.osmet_path
349 )
350 })?;
351 let mut filename: String = stem
353 .to_str()
354 .with_context(|| format!("non-UTF-8 osmet file stem: {stem:?}"))?
355 .into();
356 filename.push_str(".raw");
357 filename
358 };
359 let length = unpacker.length();
360 Ok(vec![ImageSource {
361 reader: Box::new(unpacker),
362 length_hint: Some(length),
363 signature: None,
364 filename,
365 artifact_type: "disk".to_string(),
366 }])
367 }
368
369 fn require_signature(&self) -> bool {
372 false
373 }
374}
375
376pub fn list_stream(config: ListStreamConfig) -> Result<()> {
378 #[derive(PartialEq, Eq, PartialOrd, Ord)]
379 struct Row<'a> {
380 architecture: &'a str,
381 platform: &'a str,
382 format: &'a str,
383 }
384
385 let client = new_http_client()?;
387 let stream_url = build_stream_url(&config.stream, config.stream_base_url.as_ref())?;
388 let stream = fetch_stream(client, &stream_url, FetchRetries::None)?;
389
390 let mut rows: Vec<Row> = Vec::new();
392 for (architecture_name, architecture) in stream.architectures.iter() {
393 for (platform_name, platform) in architecture.artifacts.iter() {
394 for format_name in platform.formats.keys() {
395 rows.push(Row {
396 architecture: architecture_name,
397 platform: platform_name,
398 format: format_name,
399 });
400 }
401 }
402 }
403 rows.sort();
404
405 rows.insert(
407 0,
408 Row {
409 architecture: "Architecture",
410 platform: "Platform",
411 format: "Format",
412 },
413 );
414
415 let mut widths: [usize; 2] = [0; 2];
417 for row in &rows {
418 widths[0] = widths[0].max(row.architecture.len());
419 widths[1] = widths[1].max(row.platform.len());
420 }
421
422 set_die_on_sigpipe()?;
424 for row in &rows {
425 println!(
426 "{:3$} {:4$} {}",
427 row.architecture, row.platform, row.format, widths[0], widths[1]
428 );
429 }
430 Ok(())
431}
432
433fn build_stream_url(stream: &str, base_url: Option<&Url>) -> Result<Url> {
436 base_url
437 .unwrap_or(&Url::parse(DEFAULT_STREAM_BASE_URL).unwrap())
438 .join(&format!("{stream}.json"))
439 .context("building stream URL")
440}
441
442fn fetch_stream(client: blocking::Client, url: &Url, retries: FetchRetries) -> Result<Stream> {
444 let resp = http_get(client, url, retries).context("fetching stream metadata")?;
446 match resp.status() {
447 StatusCode::OK => (),
448 s => bail!("stream metadata fetch from {} failed: {}", url, s),
449 };
450
451 let stream: Stream = serde_json::from_reader(resp).context("decoding stream metadata")?;
453 Ok(stream)
454}
455
456pub fn new_http_client() -> Result<blocking::Client> {
458 blocking::ClientBuilder::new()
459 .timeout(HTTP_COMPLETION_TIMEOUT)
460 .build()
461 .context("building HTTP client")
462}
463
464pub fn http_get(
467 client: blocking::Client,
468 url: &Url,
469 retries: FetchRetries,
470) -> Result<blocking::Response> {
471 const RETRY_STATUS_CODES: [u16; 6] = [408, 429, 500, 502, 503, 504];
473
474 let mut delay = 1;
475 let (infinite, mut tries) = match retries {
476 FetchRetries::Infinite => (true, 0),
477 FetchRetries::Finite(n) => (false, n.get() + 1),
478 FetchRetries::None => (false, 1),
479 };
480
481 loop {
482 let err: anyhow::Error = match client.get(url.clone()).send() {
483 Err(err) => err.into(),
484 Ok(resp) => match resp.status().as_u16() {
485 code if RETRY_STATUS_CODES.contains(&code) => anyhow!(
486 "HTTP {} {}",
487 code,
488 resp.status().canonical_reason().unwrap_or("")
489 ),
490 _ => {
491 return resp
492 .error_for_status()
493 .with_context(|| format!("fetching '{url}'"));
494 }
495 },
496 };
497
498 if !infinite {
499 tries -= 1;
500 if tries == 0 {
501 return Err(err).with_context(|| format!("fetching '{url}'"));
502 }
503 }
504
505 eprintln!("Error fetching '{url}': {err}");
506 eprintln!("Sleeping {delay}s and retrying...");
507 sleep(Duration::from_secs(delay));
508 delay = std::cmp::min(delay * 2, 10 * 60); }
510}
511
512#[derive(Debug, Deserialize)]
513struct Stream {
514 architectures: HashMap<String, Arch>,
515}
516
517#[derive(Debug, Deserialize)]
518struct Arch {
519 artifacts: HashMap<String, Platform>,
520}
521
522#[derive(Debug, Deserialize)]
523struct Platform {
524 formats: HashMap<String, HashMap<String, Artifact>>,
525}
526
527#[derive(Debug, Deserialize)]
528struct Artifact {
529 location: String,
530 signature: String,
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536
537 #[test]
538 fn test_new_http_client() {
539 let _ = new_http_client().unwrap();
540 }
541}