Skip to content

Commit 0d6cf54

Browse files
committed
Fix issue with missing data (.blob) needed to for valm
1 parent 97936ce commit 0d6cf54

4 files changed

Lines changed: 151 additions & 9 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "DSvClient"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
edition = "2021"
55
description = "VMware vCenter and ESXi patch downloading tool with checksum verification"
66
authors = ["Michael Ryom"]

README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,42 @@
22

33
Check out [https://michaelryom.dk/dsvclient-new-patch-downloading-tool-for-vcenter](https://michaelryom.dk/dsvclient-new-patch-downloading-tool-for-vcenter)
44

5+
## 📦 Version 0.7.0
6+
**Released:** April 11, 2026
7+
8+
### Full VCSA Patch Downloads (Bug Fix)
9+
Earlier releases downloaded only the files referenced in `manifest-latest.xml`
10+
for VCSA sources. That manifest lists RPMs and the two `*-patch-scripts.zip`
11+
files, but it does **not** include the 24+ content-addressed container image
12+
layers (`.blob`) and container manifests (`.manifest`) that a VCSA patch ships
13+
with. Without those files, `software-packages stage --iso` on the appliance
14+
fails with `"The CD drive does not have a valid patch ISO or has an unsupported
15+
version"` because the appliance can't find the container layers referenced in
16+
the patch metadata.
17+
18+
**What changed in 0.7.0:**
19+
- **New parser**`XmlParser::parse_vcsa_rpm_manifest_json()` reads
20+
`package-pool/rpm-manifest.json` and returns the complete file list that the
21+
appliance's own patching code consumes.
22+
- **New downloader hook** — after `rpm-manifest.json` is downloaded, the VCSA
23+
source loop now also parses it and queues every extra file it references
24+
(blobs + container manifests + any additional RPMs) using the same URL layout
25+
and dedup logic already used for the XML manifest entries.
26+
- **Shared queue back-end** — the per-file queueing logic in
27+
`process_vcsa_manifest` was extracted into a shared helper
28+
(`queue_vcsa_packages`) so the XML and JSON paths both go through the same
29+
verification / dedup / concurrency machinery.
30+
- **Log output** — VCSA log lines now preserve the file extension for non-RPM
31+
entries (`.blob`, `.manifest`) so they're readable in `DSvClient.log`.
32+
33+
**No configuration changes required.** Existing `sources.toml` VCSA entries
34+
work as-is — the fix activates automatically whenever `rpm-manifest.json` is in
35+
the `files = [...]` list (which it already is in the default config).
36+
37+
Result: a VCSA patch folder downloaded with 0.7.0 contains everything
38+
`software-packages stage --iso --acceptEulas` needs to validate and stage the
39+
patch on the appliance.
40+
541
## 🔐 Version 0.6.0
642
**Released:** August 27, 2025
743

@@ -165,4 +201,4 @@ Each platform release now includes:
165201
- **Improved config file handling** - Better integration with release packages
166202

167203
### Version Progression
168-
- v0.3.0 → v0.3.1 → v0.4.0 → v0.5.0 → v0.6.0 (current)
204+
- v0.3.0 → v0.3.1 → v0.4.0 → v0.5.0 → v0.6.0 → v0.7.0 (current)

src/downloader.rs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::config::AppConfig;
2-
use crate::parser::{Vendor, XmlParser};
2+
use crate::parser::{Vendor, VcsaPackage, XmlParser};
33
use crate::process::{FileType, ProcessManager, Source};
44
use crate::verify::VerificationManager;
55
use anyhow::Result;
@@ -454,6 +454,24 @@ impl Downloader {
454454
Err(e) => warn!("Error reading manifest file: {}", e),
455455
}
456456
}
457+
458+
// rpm-manifest.json lists additional files that are NOT referenced in
459+
// manifest-latest.xml - notably container image blobs (.blob) and
460+
// container manifests (.manifest). Without these, `software-packages
461+
// stage --iso` fails on the VCSA because the stage step can't find
462+
// the container layers referenced by the patch metadata.
463+
if file.ends_with("rpm-manifest.json") {
464+
match tokio::fs::read_to_string(&target_path).await {
465+
Ok(json_content) => {
466+
if let Err(e) = this.process_vcsa_rpm_manifest_json(&json_content, &version, &processed_base_url).await {
467+
warn!("Error processing rpm-manifest.json: {}", e);
468+
} else {
469+
info!("Successfully processed VCSA rpm-manifest.json");
470+
}
471+
}
472+
Err(e) => warn!("Error reading rpm-manifest.json file: {}", e),
473+
}
474+
}
457475
}
458476
} else {
459477
warn!("VCSA source missing version or files: {}", url);
@@ -1482,21 +1500,45 @@ impl Downloader {
14821500
}
14831501

14841502
async fn process_vcsa_manifest(&self, content: &str, version: &str, base_url: &str) -> Result<()> {
1485-
let start_time = std::time::Instant::now();
14861503
let packages = self.xml_parser.parse_vcsa_packages(content)?;
1487-
info!("Found {} packages in VCSA manifest for version {}", packages.len(), version);
1504+
info!("Found {} packages in VCSA manifest-latest.xml for version {}", packages.len(), version);
1505+
self.queue_vcsa_packages(packages, version, base_url, "manifest-latest.xml").await
1506+
}
1507+
1508+
/// Parse VCSA rpm-manifest.json and queue any files it lists that aren't
1509+
/// already covered by manifest-latest.xml (blobs, container manifests, and
1510+
/// any extra RPMs listed only in the JSON).
1511+
async fn process_vcsa_rpm_manifest_json(&self, content: &str, version: &str, base_url: &str) -> Result<()> {
1512+
let packages = self.xml_parser.parse_vcsa_rpm_manifest_json(content)?;
1513+
info!("Found {} packages in VCSA rpm-manifest.json for version {}", packages.len(), version);
1514+
self.queue_vcsa_packages(packages, version, base_url, "rpm-manifest.json").await
1515+
}
1516+
1517+
/// Shared back-end for processing a list of VcsaPackage entries: builds URLs,
1518+
/// skips already-processed / already-valid files, and spawns concurrent
1519+
/// download tasks.
1520+
async fn queue_vcsa_packages(
1521+
&self,
1522+
packages: Vec<VcsaPackage>,
1523+
version: &str,
1524+
base_url: &str,
1525+
source_label: &str,
1526+
) -> Result<()> {
1527+
let start_time = std::time::Instant::now();
14881528

14891529
let mut tasks = Vec::new();
14901530
let max_concurrent = self.verifier.config.verification.max_concurrent_files();
14911531
let file_semaphore = Arc::new(Semaphore::new(max_concurrent));
14921532

14931533
for package in packages {
14941534
let location = package.location.clone();
1535+
// pkg_info is only used for log output - use the filename with the
1536+
// package-pool/ prefix stripped, keeping the file extension so that
1537+
// non-RPM entries (like .blob / .manifest) are still readable in logs.
14951538
let pkg_info = location
14961539
.strip_prefix("package-pool/")
1497-
.and_then(|s| s.strip_suffix(".rpm"))
14981540
.unwrap_or(&location);
1499-
1541+
15001542
let url = format!(
15011543
"{}/{}/{}",
15021544
base_url,
@@ -1598,8 +1640,8 @@ impl Downloader {
15981640
}
15991641

16001642
let duration = start_time.elapsed();
1601-
info!("VCSA manifest processing completed in {:?}: {}/{} packages successful, {} errors",
1602-
duration, completed, total_tasks, errors);
1643+
info!("VCSA {} processing completed in {:?}: {}/{} packages successful, {} errors",
1644+
source_label, duration, completed, total_tasks, errors);
16031645

16041646
Ok(())
16051647
}

src/parser.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,70 @@ impl XmlParser {
469469
Ok(quick_xml::de::from_str(content)?)
470470
}
471471

472+
/// Parse the VCSA rpm-manifest.json file.
473+
///
474+
/// This file is a JSON dict keyed by SHA256, with entries like:
475+
///
476+
/// ```json
477+
/// {
478+
/// "files": {
479+
/// "015dc347...c72565": {
480+
/// "relativepath": "015dc347...c72565.blob",
481+
/// "sha256val": "015dc347...c72565",
482+
/// "type": "blob"
483+
/// },
484+
/// "bba8b3bf...1aab5": {
485+
/// "relativepath": "Linux-PAM-1.5.3-6.ph4.x86_64.rpm",
486+
/// "sha256val": "bba8b3bf...1aab5",
487+
/// "type": "rpm"
488+
/// }
489+
/// }
490+
/// }
491+
/// ```
492+
///
493+
/// rpm-manifest.json lists extra files that are NOT in manifest-latest.xml -
494+
/// notably container image layers (`.blob`) and container manifests
495+
/// (`.manifest`). These files are required for `software-packages stage --iso`
496+
/// to succeed on the VCSA during appliance patching.
497+
///
498+
/// The `relativepath` values in this file are bare filenames (no
499+
/// `package-pool/` prefix), so we prepend `package-pool/` to match the URL
500+
/// layout the downloader already uses for entries from the XML manifest.
501+
pub fn parse_vcsa_rpm_manifest_json(&self, content: &str) -> Result<Vec<VcsaPackage>> {
502+
let v: serde_json::Value = serde_json::from_str(content)?;
503+
let files = v.get("files")
504+
.and_then(|f| f.as_object())
505+
.ok_or_else(|| anyhow::anyhow!("rpm-manifest.json is missing a top-level 'files' object"))?;
506+
507+
let mut packages = Vec::with_capacity(files.len());
508+
for (_hash, entry) in files {
509+
let relpath = entry.get("relativepath")
510+
.and_then(|v| v.as_str())
511+
.unwrap_or("")
512+
.to_string();
513+
if relpath.is_empty() {
514+
continue;
515+
}
516+
let sha256 = entry.get("sha256val")
517+
.and_then(|v| v.as_str())
518+
.unwrap_or("")
519+
.to_string();
520+
521+
packages.push(VcsaPackage {
522+
name: relpath.clone(),
523+
// Prepend package-pool/ so existing URL-building in downloader.rs
524+
// (format!("{base}/{version}/{location}")) resolves to the right URL.
525+
location: format!("package-pool/{}", relpath),
526+
version: String::new(),
527+
arch: String::new(),
528+
checksum: String::new(),
529+
checksum256: sha256,
530+
});
531+
}
532+
533+
Ok(packages)
534+
}
535+
472536
pub fn parse_vcsa_packages(&self, content: &str) -> Result<Vec<VcsaPackage>> {
473537
let mut reader = quick_xml::Reader::from_str(content);
474538
let mut buf = Vec::new();

0 commit comments

Comments
 (0)