[fix] VerNum
This commit is contained in:
parent
68cedff1d9
commit
0827882bb3
30 changed files with 1651 additions and 240 deletions
10
iota-installer/bundle-files.txt
Normal file
10
iota-installer/bundle-files.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
bin/iota
|
||||
bin/iota-daemon
|
||||
bin/iota-updater
|
||||
systemd/iota-daemon.service
|
||||
systemd/iota-daemon.socket
|
||||
systemd/sysusers.d/iota.conf
|
||||
systemd/iota-update.service
|
||||
systemd/iota-update.timer
|
||||
systemd/update.env
|
||||
manifest.json
|
||||
12
iota-installer/src/bin/iota-bundle.rs
Normal file
12
iota-installer/src/bin/iota-bundle.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use anyhow::{Context, Result, bail};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut arguments = std::env::args_os();
|
||||
let _program = arguments.next();
|
||||
let bundle = arguments.next().context("usage: iota-bundle BUNDLE.zip")?;
|
||||
if arguments.next().is_some() {
|
||||
bail!("usage: iota-bundle BUNDLE.zip");
|
||||
}
|
||||
iota_installer::validate_linux_bundle(Path::new(&bundle))
|
||||
}
|
||||
|
|
@ -3,17 +3,16 @@ use std::{fs, io, path::Path, process::Command};
|
|||
use tempfile::tempdir;
|
||||
use zip::ZipArchive;
|
||||
|
||||
const REQUIRED: &[&str] = &[
|
||||
"bin/iota",
|
||||
"bin/iota-daemon",
|
||||
"bin/iota-updater",
|
||||
"systemd/iota-daemon.service",
|
||||
"systemd/iota-daemon.socket",
|
||||
"systemd/sysusers.d/iota.conf",
|
||||
"systemd/iota-update.service",
|
||||
"systemd/iota-update.timer",
|
||||
"manifest.json",
|
||||
];
|
||||
const REQUIRED: &str = include_str!("../bundle-files.txt");
|
||||
const DAEMON_EXECUTABLE_PLACEHOLDER: &str = "@IOTA_SYSTEM_DAEMON_EXECUTABLE@";
|
||||
|
||||
pub fn validate_linux_bundle(bundle: &Path) -> Result<()> {
|
||||
let staging = tempdir().context("create bundle validation directory")?;
|
||||
extract_linux_bundle(bundle, staging.path())?;
|
||||
product_version(staging.path())?;
|
||||
validate_update_environment(staging.path())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn install_linux_bundle(bundle: &Path) -> Result<()> {
|
||||
install_linux_bundle_with_operator(bundle, None)
|
||||
|
|
@ -28,55 +27,39 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
|
|||
bail!("Linux systemd bundles are not supported on this platform");
|
||||
}
|
||||
let staging = tempdir().context("create installer staging directory")?;
|
||||
let file = fs::File::open(bundle).context("open release bundle")?;
|
||||
let mut archive = ZipArchive::new(file).context("read release bundle")?;
|
||||
for name in REQUIRED {
|
||||
let mut entry = archive
|
||||
.by_name(name)
|
||||
.with_context(|| format!("bundle is missing {name}"))?;
|
||||
let output = staging.path().join(name);
|
||||
if let Some(parent) = output.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut out = fs::File::create(&output)?;
|
||||
io::copy(&mut entry, &mut out)?;
|
||||
}
|
||||
extract_linux_bundle(bundle, staging.path())?;
|
||||
let product_version = product_version(staging.path())?;
|
||||
validate_update_environment(staging.path())?;
|
||||
|
||||
render_daemon_service(&staging.path().join("systemd/iota-daemon.service"))?;
|
||||
|
||||
let version_dir = format!(
|
||||
"{}/versions/{product_version}",
|
||||
iota_paths::install_root().display()
|
||||
);
|
||||
install(
|
||||
&staging.path().join("bin/iota"),
|
||||
&format!(
|
||||
"{}/versions/{}/bin/iota",
|
||||
iota_paths::install_root().display(),
|
||||
product_version(staging.path())
|
||||
),
|
||||
&format!("{version_dir}/bin/iota"),
|
||||
"0755",
|
||||
)?;
|
||||
install(
|
||||
&staging.path().join("bin/iota-daemon"),
|
||||
&format!(
|
||||
"{}/versions/{}/bin/iota-daemon",
|
||||
iota_paths::install_root().display(),
|
||||
product_version(staging.path())
|
||||
),
|
||||
&format!("{version_dir}/bin/iota-daemon"),
|
||||
"0755",
|
||||
)?;
|
||||
let version_dir = format!(
|
||||
"{}/versions/{}",
|
||||
iota_paths::install_root().display(),
|
||||
product_version(staging.path())
|
||||
);
|
||||
if !Path::new(&format!("{version_dir}/bin/iota-daemon")).is_file() {
|
||||
bail!("installed daemon executable is missing: {version_dir}/bin/iota-daemon");
|
||||
}
|
||||
install(
|
||||
&staging.path().join("bin/iota-updater"),
|
||||
&format!(
|
||||
"{}/versions/{}/bin/iota-updater",
|
||||
iota_paths::install_root().display(),
|
||||
product_version(staging.path())
|
||||
),
|
||||
&format!("{version_dir}/bin/iota-updater"),
|
||||
"0755",
|
||||
)?;
|
||||
install(
|
||||
&staging.path().join("manifest.json"),
|
||||
&format!("{version_dir}/manifest.json"),
|
||||
"0644",
|
||||
)?;
|
||||
for unit in [
|
||||
"iota-daemon.service",
|
||||
"iota-daemon.socket",
|
||||
|
|
@ -94,6 +77,11 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
|
|||
"/etc/sysusers.d/iota.conf",
|
||||
"0644",
|
||||
)?;
|
||||
install(
|
||||
&staging.path().join("systemd/update.env"),
|
||||
"/etc/iota/update.env",
|
||||
"0644",
|
||||
)?;
|
||||
run(
|
||||
"ln",
|
||||
&[
|
||||
|
|
@ -118,7 +106,7 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
|
|||
"{}/current/bin/iota-daemon",
|
||||
iota_paths::install_root().display()
|
||||
),
|
||||
"/usr/local/libexec/iota/iota-daemon",
|
||||
iota_paths::SYSTEM_DAEMON_EXECUTABLE,
|
||||
],
|
||||
)?;
|
||||
run("systemd-sysusers", &[])?;
|
||||
|
|
@ -138,8 +126,10 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
|
|||
}
|
||||
run("systemctl", &["daemon-reload"])?;
|
||||
run("systemctl", &["enable", "--now", "iota-daemon.socket"])?;
|
||||
run("systemctl", &["enable", "--now", "iota-update.timer"])?;
|
||||
run("systemctl", &["is-active", "iota-daemon.socket"])?;
|
||||
run("systemctl", &["is-enabled", "iota-daemon.socket"])?;
|
||||
run("systemctl", &["is-enabled", "iota-update.timer"])?;
|
||||
let socket = iota_paths::socket_path(iota_paths::Scope::System);
|
||||
if !socket.exists() {
|
||||
bail!(
|
||||
|
|
@ -150,17 +140,107 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn product_version(staging: &Path) -> String {
|
||||
fs::read_to_string(staging.join("manifest.json"))
|
||||
.ok()
|
||||
.and_then(|value| serde_json::from_str::<serde_json::Value>(&value).ok())
|
||||
.and_then(|value| {
|
||||
value
|
||||
.get("product_version")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_owned)
|
||||
fn extract_linux_bundle(bundle: &Path, staging: &Path) -> Result<()> {
|
||||
let file = fs::File::open(bundle).context("open release bundle")?;
|
||||
let mut archive = ZipArchive::new(file).context("read release bundle")?;
|
||||
for name in REQUIRED.lines().filter(|name| !name.is_empty()) {
|
||||
let mut entry = archive
|
||||
.by_name(name)
|
||||
.with_context(|| format!("bundle is missing {name}"))?;
|
||||
let output = staging.join(name);
|
||||
if let Some(parent) = output.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let mut out = fs::File::create(&output)?;
|
||||
io::copy(&mut entry, &mut out)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_daemon_service(path: &Path) -> Result<()> {
|
||||
let template = fs::read_to_string(path)
|
||||
.with_context(|| format!("read systemd unit template {}", path.display()))?;
|
||||
let placeholder_count = template.matches(DAEMON_EXECUTABLE_PLACEHOLDER).count();
|
||||
if placeholder_count != 1 {
|
||||
bail!(
|
||||
"systemd unit template {} must contain exactly one {DAEMON_EXECUTABLE_PLACEHOLDER} placeholder, found {placeholder_count}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
fs::write(
|
||||
path,
|
||||
template.replace(
|
||||
DAEMON_EXECUTABLE_PLACEHOLDER,
|
||||
iota_paths::SYSTEM_DAEMON_EXECUTABLE,
|
||||
),
|
||||
)
|
||||
.with_context(|| format!("render systemd unit {}", path.display()))
|
||||
}
|
||||
|
||||
fn product_version(staging: &Path) -> Result<String> {
|
||||
let manifest_path = staging.join("manifest.json");
|
||||
let contents = fs::read_to_string(&manifest_path)
|
||||
.with_context(|| format!("read bundle manifest {}", manifest_path.display()))?;
|
||||
let manifest: serde_json::Value = serde_json::from_str(&contents)
|
||||
.with_context(|| format!("parse bundle manifest {}", manifest_path.display()))?;
|
||||
let version = manifest
|
||||
.get("product_version")
|
||||
.and_then(|value| value.as_str())
|
||||
.filter(|value| !value.is_empty())
|
||||
.context("bundle manifest product_version must be a non-empty string")?;
|
||||
let mut characters = version.chars();
|
||||
if !characters
|
||||
.next()
|
||||
.is_some_and(|character| character.is_ascii_alphanumeric())
|
||||
|| !characters.all(|character| {
|
||||
character.is_ascii_alphanumeric() || matches!(character, '.' | '+' | '_' | '-')
|
||||
})
|
||||
.unwrap_or_else(|| "unversioned".into())
|
||||
{
|
||||
bail!("bundle manifest product_version contains unsupported characters");
|
||||
}
|
||||
Ok(version.to_owned())
|
||||
}
|
||||
|
||||
fn validate_update_environment(staging: &Path) -> Result<()> {
|
||||
let path = staging.join("systemd/update.env");
|
||||
let contents = fs::read_to_string(&path)
|
||||
.with_context(|| format!("read updater environment {}", path.display()))?;
|
||||
let mut entries = std::collections::BTreeMap::new();
|
||||
for line in contents.lines().filter(|line| !line.is_empty()) {
|
||||
let (name, value) = line
|
||||
.split_once('=')
|
||||
.with_context(|| format!("invalid updater environment entry in {}", path.display()))?;
|
||||
if entries.insert(name, value).is_some() {
|
||||
bail!("duplicate updater environment variable {name}");
|
||||
}
|
||||
}
|
||||
for name in [
|
||||
"IOTA_UPDATE_MANIFEST",
|
||||
"IOTA_UPDATE_PUBLIC_KEY",
|
||||
"IOTA_UPDATE_SIGNATURE",
|
||||
] {
|
||||
let value = entries
|
||||
.get(name)
|
||||
.filter(|value| !value.is_empty())
|
||||
.with_context(|| format!("updater environment is missing {name}"))?;
|
||||
if value.chars().any(char::is_whitespace) {
|
||||
bail!("updater environment variable {name} contains whitespace");
|
||||
}
|
||||
}
|
||||
if entries.len() != 3 {
|
||||
bail!("updater environment contains unexpected variables");
|
||||
}
|
||||
let public_key = entries
|
||||
.get("IOTA_UPDATE_PUBLIC_KEY")
|
||||
.context("updater environment is missing IOTA_UPDATE_PUBLIC_KEY")?;
|
||||
if public_key.len() != 64
|
||||
|| !public_key
|
||||
.chars()
|
||||
.all(|character| character.is_ascii_hexdigit())
|
||||
{
|
||||
bail!("IOTA_UPDATE_PUBLIC_KEY must be 32-byte hex");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install(source: &Path, destination: &str, mode: &str) -> Result<()> {
|
||||
|
|
@ -181,3 +261,97 @@ fn run(program: &str, args: &[&str]) -> Result<()> {
|
|||
bail!("{program} failed; run the installer as root")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use zip::{ZipWriter, write::SimpleFileOptions};
|
||||
|
||||
fn write_bundle(path: &Path, omitted: Option<&str>, product_version: &str) {
|
||||
let file = fs::File::create(path).unwrap();
|
||||
let mut archive = ZipWriter::new(file);
|
||||
for name in REQUIRED.lines().filter(|name| !name.is_empty()) {
|
||||
if omitted == Some(name) {
|
||||
continue;
|
||||
}
|
||||
archive
|
||||
.start_file(name, SimpleFileOptions::default())
|
||||
.unwrap();
|
||||
if name == "manifest.json" {
|
||||
write!(archive, "{{\"product_version\":\"{product_version}\"}}").unwrap();
|
||||
} else if name == "systemd/update.env" {
|
||||
archive
|
||||
.write_all(
|
||||
b"IOTA_UPDATE_MANIFEST=https://example.invalid/manifest.json\nIOTA_UPDATE_PUBLIC_KEY=0707070707070707070707070707070707070707070707070707070707070707\nIOTA_UPDATE_SIGNATURE=https://example.invalid/manifest.json.sig\n",
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
archive.write_all(b"bundle member").unwrap();
|
||||
}
|
||||
}
|
||||
archive.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daemon_service_uses_the_installed_daemon_entry_point() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let unit = directory.path().join("iota-daemon.service");
|
||||
fs::write(&unit, include_str!("../../systemd/iota-daemon.service")).unwrap();
|
||||
|
||||
render_daemon_service(&unit).unwrap();
|
||||
|
||||
let rendered = fs::read_to_string(unit).unwrap();
|
||||
assert!(rendered.contains(&format!(
|
||||
"ExecStart={}",
|
||||
iota_paths::SYSTEM_DAEMON_EXECUTABLE
|
||||
)));
|
||||
assert!(!rendered.contains(DAEMON_EXECUTABLE_PLACEHOLDER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_complete_bundle() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let bundle = directory.path().join("release.zip");
|
||||
write_bundle(&bundle, None, "0.1.0-dev-abcdef0");
|
||||
|
||||
validate_linux_bundle(&bundle).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_bundle_missing_contract_member() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let bundle = directory.path().join("release.zip");
|
||||
write_bundle(&bundle, Some("systemd/iota-update.timer"), "0.1.0");
|
||||
|
||||
let error = validate_linux_bundle(&bundle).unwrap_err();
|
||||
assert!(error.to_string().contains("systemd/iota-update.timer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsafe_product_version() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let bundle = directory.path().join("release.zip");
|
||||
write_bundle(&bundle, None, "../../outside");
|
||||
|
||||
let error = validate_linux_bundle(&bundle).unwrap_err();
|
||||
assert!(error.to_string().contains("unsupported characters"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_bundle_without_complete_updater_environment() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let systemd = directory.path().join("systemd");
|
||||
fs::create_dir_all(&systemd).unwrap();
|
||||
let environment = systemd.join("update.env");
|
||||
fs::write(
|
||||
&environment,
|
||||
"IOTA_UPDATE_MANIFEST=https://example.invalid/manifest.json\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let error = validate_update_environment(directory.path()).unwrap_err();
|
||||
|
||||
assert!(error.to_string().contains("IOTA_UPDATE_PUBLIC_KEY"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue