working ipv6 ip assignment + ssh with login/passwd
This commit is contained in:
@@ -408,27 +408,35 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
let dhcp_end =
|
||||
std::env::var("HERO_VIRT_DHCP_END").unwrap_or_else(|_| "172.30.0.250".into());
|
||||
|
||||
// Optional IPv6 over Mycelium provisioning (bridge P::2/64 + RA) guarded by env and detection.
|
||||
// IPv6 over Mycelium: enabled by default.
|
||||
// If explicitly disabled via HERO_VIRT_IPV6_ENABLE=false|0, we skip.
|
||||
// If enabled but Mycelium is not detected, return an error.
|
||||
let ipv6_env = std::env::var("HERO_VIRT_IPV6_ENABLE").unwrap_or_else(|_| "".into());
|
||||
let mut ipv6_enabled = ipv6_env.eq_ignore_ascii_case("1") || ipv6_env.eq_ignore_ascii_case("true");
|
||||
let ipv6_requested = match ipv6_env.to_lowercase().as_str() {
|
||||
"" | "1" | "true" | "yes" => true,
|
||||
"0" | "false" | "no" => false,
|
||||
_ => true,
|
||||
};
|
||||
let mycelium_if_cfg = std::env::var("HERO_VIRT_MYCELIUM_IF").unwrap_or_else(|_| "mycelium".into());
|
||||
let mut ipv6_bridge_cidr: Option<String> = None;
|
||||
let mut mycelium_if_opt: Option<String> = None;
|
||||
let enable_auto = ipv6_env.is_empty(); // auto-enable if mycelium is detected and not explicitly disabled
|
||||
|
||||
if ipv6_enabled || enable_auto {
|
||||
if ipv6_requested {
|
||||
if let Ok(cidr) = std::env::var("HERO_VIRT_IPV6_BRIDGE_CIDR") {
|
||||
// Explicit override for bridge IPv6 (e.g., "400:...::2/64")
|
||||
// Explicit override for bridge IPv6 (e.g., "400:...::2/64") but still require mycelium iface presence.
|
||||
// Validate mycelium interface and that it has IPv6 configured.
|
||||
let _ = get_mycelium_ipv6_addr(&mycelium_if_cfg)?; // returns DependencyMissing on failure
|
||||
ipv6_bridge_cidr = Some(cidr);
|
||||
mycelium_if_opt = Some(mycelium_if_cfg.clone());
|
||||
ipv6_enabled = true;
|
||||
} else if let Ok((ifname, myc_addr)) = get_mycelium_ipv6_addr(&mycelium_if_cfg) {
|
||||
// Derive P::2/64 from the mycelium node address
|
||||
if let Ok((_pfx, router_cidr)) = derive_ipv6_prefix_from_mycelium(&myc_addr) {
|
||||
ipv6_bridge_cidr = Some(router_cidr);
|
||||
mycelium_if_opt = Some(ifname);
|
||||
ipv6_enabled = true;
|
||||
}
|
||||
} else {
|
||||
// Auto-derive from mycelium node address; error out if not detected.
|
||||
println!("auto-deriving mycelium address...");
|
||||
let (ifname, myc_addr) = get_mycelium_ipv6_addr(&mycelium_if_cfg)?;
|
||||
println!("on if {ifname}, got myc addr: {myc_addr}");
|
||||
let (_pfx, router_cidr) = derive_ipv6_prefix_from_mycelium(&myc_addr)?;
|
||||
println!("derived pfx: {_pfx} and router cidr: {router_cidr}");
|
||||
ipv6_bridge_cidr = Some(router_cidr);
|
||||
mycelium_if_opt = Some(ifname);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,8 +453,10 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
|
||||
// Ensure a TAP device for this VM and attach to the bridge
|
||||
let tap_name = ensure_tap_for_vm(&bridge_name, id)?;
|
||||
println!("TAP device for vm called: {tap_name}");
|
||||
// Stable locally-administered MAC derived from VM id
|
||||
let mac = stable_mac_from_id(id);
|
||||
println!("MAC for vm: {mac}");
|
||||
|
||||
parts.push("--net".into());
|
||||
parts.push(format!("tap={},mac={}", tap_name, mac));
|
||||
@@ -465,6 +475,7 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
log_file,
|
||||
vm_pid_path(id).to_string_lossy()
|
||||
);
|
||||
println!("executing command:\n{heredoc}");
|
||||
// Execute command; this will background cloud-hypervisor and return
|
||||
let result = sal_process::run(&heredoc).execute();
|
||||
match result {
|
||||
@@ -489,6 +500,7 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
Ok(s) => s.trim().parse::<i64>().ok(),
|
||||
Err(_) => None,
|
||||
};
|
||||
println!("reading PID back: {} - (if 0 == not found)", pid.unwrap_or(0));
|
||||
|
||||
// Quick health check: ensure process did not exit immediately due to CLI errors (e.g., duplicate flags)
|
||||
if let Some(pid_num) = pid {
|
||||
@@ -496,6 +508,7 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
if !proc_exists(pid_num) {
|
||||
// Tail log to surface the error cause
|
||||
let tail_cmd = format!("tail -n 200 {}", shell_escape(&log_file));
|
||||
println!("executing tail_cmd command:\n{tail_cmd}");
|
||||
let tail = sal_process::run(&tail_cmd).die(false).silent(true).execute();
|
||||
let mut log_snip = String::new();
|
||||
if let Ok(res) = tail {
|
||||
@@ -524,6 +537,76 @@ pub fn vm_start(id: &str) -> Result<(), CloudHvError> {
|
||||
|
||||
let value = serde_json::to_value(&rec).map_err(|e| CloudHvError::JsonError(e.to_string()))?;
|
||||
write_json(&vm_json_path(id), &value)?;
|
||||
println!("wrote JSON for VM");
|
||||
|
||||
// Best-effort: discover and print guest IPv4/IPv6 addresses (default-net path)
|
||||
// Give DHCP/ND a moment
|
||||
println!("waiting 5 secs for DHCP/ND");
|
||||
thread::sleep(Duration::from_millis(5000));
|
||||
let bridge_name = std::env::var("HERO_VIRT_BRIDGE_NAME").unwrap_or_else(|_| "br-hero".into());
|
||||
let mac_lower = stable_mac_from_id(id).to_lowercase();
|
||||
|
||||
// IPv4 from dnsmasq leases (pinned per-bridge leasefile)
|
||||
// Path set in ensure_host_net_prereq_dnsmasq_nftables: /var/lib/misc/dnsmasq-hero-$BR.leases
|
||||
let lease_path = std::env::var("HERO_VIRT_DHCP_LEASE_FILE")
|
||||
.unwrap_or_else(|_| format!("/var/lib/misc/dnsmasq-hero-{}.leases", bridge_name));
|
||||
// Parse dnsmasq leases directly to avoid shell quoting/pipelines
|
||||
let ipv4 = (|| {
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(12);
|
||||
loop {
|
||||
if let Ok(content) = fs::read_to_string(&lease_path) {
|
||||
let mut last_ip: Option<String> = None;
|
||||
for line in content.lines() {
|
||||
let cols: Vec<&str> = line.split_whitespace().collect();
|
||||
if cols.len() >= 3 && cols[1].eq_ignore_ascii_case(&mac_lower) {
|
||||
last_ip = Some(cols[2].to_string());
|
||||
}
|
||||
}
|
||||
if last_ip.is_some() {
|
||||
return last_ip;
|
||||
}
|
||||
}
|
||||
if std::time::Instant::now() >= deadline {
|
||||
return None;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(800));
|
||||
}
|
||||
})();
|
||||
println!(
|
||||
"Got IPv4 from dnsmasq lease ({}): {}",
|
||||
lease_path,
|
||||
ipv4.clone().unwrap_or("not found".to_string())
|
||||
);
|
||||
|
||||
// IPv6 from neighbor table on the bridge (exclude link-local), parsed in Rust
|
||||
let ipv6 = (|| {
|
||||
let cmd = format!("ip -6 neigh show dev {}", bridge_name);
|
||||
if let Ok(res) = sal_process::run(&cmd).silent(true).die(false).execute() {
|
||||
if res.success {
|
||||
let mac_pat = format!("lladdr {}", mac_lower);
|
||||
for line in res.stdout.lines() {
|
||||
let lt = line.trim();
|
||||
if lt.to_lowercase().contains(&mac_pat) {
|
||||
if let Some(addr) = lt.split_whitespace().next() {
|
||||
if !addr.starts_with("fe80") && !addr.is_empty() {
|
||||
return Some(addr.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})();
|
||||
|
||||
println!("Got IPv6 from neighbor table on bridge: {}", ipv6.clone().unwrap_or("not found".to_string()));
|
||||
|
||||
println!(
|
||||
"[cloudhv] VM '{}' guest addresses: IPv4={}, IPv6={}",
|
||||
id,
|
||||
ipv4.as_deref().unwrap_or(""),
|
||||
ipv6.as_deref().unwrap_or("")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -711,50 +794,44 @@ fn stable_mac_from_id(id: &str) -> String {
|
||||
format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", b0, b1, b2, b3, b4, b5)
|
||||
}
|
||||
|
||||
/// Discover the mycelium IPv6 address and validate the interface.
|
||||
///
|
||||
/// Returns (interface_name, address_string).
|
||||
/// Discover the mycelium IPv6 address by inspecting the interface itself (no CLI dependency).
|
||||
/// Returns (interface_name, first global IPv6 address found on the interface).
|
||||
fn get_mycelium_ipv6_addr(iface_hint: &str) -> Result<(String, String), CloudHvError> {
|
||||
// Parse `mycelium inspect` for "Address: ..."
|
||||
let insp = sal_process::run("mycelium inspect").silent(true).die(false).execute();
|
||||
let mut addr: Option<String> = None;
|
||||
if let Ok(res) = insp {
|
||||
if res.success {
|
||||
for l in res.stdout.lines() {
|
||||
let lt = l.trim();
|
||||
if let Some(rest) = lt.strip_prefix("Address:") {
|
||||
let val = rest.trim().trim_matches('"').to_string();
|
||||
if !val.is_empty() {
|
||||
addr = Some(val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let iface = std::env::var("HERO_VIRT_MYCELIUM_IF").unwrap_or_else(|_| iface_hint.to_string());
|
||||
|
||||
// Validate interface exists and has IPv6 configured (best-effort)
|
||||
// Query IPv6 addresses on the interface
|
||||
let cmd = format!("ip -6 addr show dev {}", shell_escape(&iface));
|
||||
match sal_process::run(&cmd).silent(true).die(false).execute() {
|
||||
Ok(r) if r.success => {
|
||||
// proceed
|
||||
}
|
||||
let res = sal_process::run(&cmd).silent(true).die(false).execute();
|
||||
let out = match res {
|
||||
Ok(r) if r.success => r.stdout,
|
||||
_ => {
|
||||
return Err(CloudHvError::DependencyMissing(format!(
|
||||
"mycelium interface '{}' not found or no IPv6 configured",
|
||||
iface
|
||||
)));
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// Extract the first global IPv6 address present on the interface.
|
||||
for line in out.lines() {
|
||||
let lt = line.trim();
|
||||
// Example line: "inet6 578:9fcf:.../7 scope global"
|
||||
if lt.starts_with("inet6 ") && lt.contains("scope global") {
|
||||
let parts: Vec<&str> = lt.split_whitespace().collect();
|
||||
if let Some(addr_cidr) = parts.get(1) {
|
||||
let addr_only = addr_cidr.split('/').next().unwrap_or("").trim();
|
||||
println!("got addr from host: {addr_only}");
|
||||
if !addr_only.is_empty() && addr_only.parse::<std::net::Ipv6Addr>().is_ok() {
|
||||
return Ok((iface, addr_only.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(a) = addr {
|
||||
Ok((iface, a))
|
||||
} else {
|
||||
Err(CloudHvError::DependencyMissing(
|
||||
"failed to read mycelium IPv6 address via `mycelium inspect`".into(),
|
||||
))
|
||||
}
|
||||
Err(CloudHvError::DependencyMissing(format!(
|
||||
"no global IPv6 found on interface '{}'",
|
||||
iface
|
||||
)))
|
||||
}
|
||||
|
||||
/// Derive a /64 prefix P from the mycelium IPv6 and return (P/64, P::2/64).
|
||||
@@ -803,10 +880,15 @@ SUBNET={subnet}
|
||||
DHCP_START={dstart}
|
||||
DHCP_END={dend}
|
||||
IPV6_CIDR={v6cidr}
|
||||
LEASE_FILE=/var/lib/misc/dnsmasq-hero-$BR.leases
|
||||
|
||||
# Determine default WAN interface
|
||||
WAN_IF=$(ip -o route show default | awk '{{print $5}}' | head -n1)
|
||||
|
||||
if [ -z \"$WAN_IF\" ]; then
|
||||
echo \"No default WAN interface detected (required for IPv4 NAT)\" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Bridge creation (idempotent)
|
||||
ip link show \"$BR\" >/dev/null 2>&1 || ip link add name \"$BR\" type bridge
|
||||
ip addr replace \"$BR_ADDR\" dev \"$BR\"
|
||||
@@ -829,24 +911,38 @@ nft list chain ip hero postrouting | grep -q \"ip saddr $SUBNET oifname \\\"$WAN
|
||||
|
||||
# dnsmasq DHCPv4 + RA/DHCPv6 config (idempotent)
|
||||
mkdir -p /etc/dnsmasq.d
|
||||
mkdir -p /var/lib/misc
|
||||
CFG=/etc/dnsmasq.d/hero-$BR.conf
|
||||
TMP=/etc/dnsmasq.d/.hero-$BR.conf.new
|
||||
|
||||
# Always include IPv4 section
|
||||
cat >\"$TMP\" <<EOF
|
||||
interface=$BR
|
||||
bind-interfaces
|
||||
dhcp-range=$DHCP_START,$DHCP_END,12h
|
||||
dhcp-option=option:dns-server,1.1.1.1,8.8.8.8
|
||||
EOF
|
||||
RELOAD=0
|
||||
CONF=/etc/dnsmasq.conf
|
||||
# Ensure conf-dir includes /etc/dnsmasq.d (simple fixed-string check to avoid regex escapes in Rust)
|
||||
if ! grep -qF \"conf-dir=/etc/dnsmasq.d\" \"$CONF\"; then
|
||||
printf '%s\n' 'conf-dir=/etc/dnsmasq.d,*.conf' >> \"$CONF\"
|
||||
RELOAD=1
|
||||
fi
|
||||
|
||||
|
||||
# Ensure lease file exists and is writable by dnsmasq user
|
||||
touch \"$LEASE_FILE\" || true
|
||||
chown dnsmasq:dnsmasq \"$LEASE_FILE\" 2>/dev/null || true
|
||||
|
||||
# Always include IPv4 section
|
||||
printf '%s\n' \
|
||||
\"interface=$BR\" \
|
||||
\"bind-interfaces\" \
|
||||
\"dhcp-authoritative\" \
|
||||
\"dhcp-range=$DHCP_START,$DHCP_END,12h\" \
|
||||
\"dhcp-option=option:dns-server,1.1.1.1,8.8.8.8\" \
|
||||
\"dhcp-leasefile=$LEASE_FILE\" >\"$TMP\"
|
||||
|
||||
# Optionally append IPv6 RA/DHCPv6
|
||||
if [ -n \"$IPV6_CIDR\" ]; then
|
||||
cat >>\"$TMP\" <<'EOFV6'
|
||||
enable-ra
|
||||
dhcp-range=::,constructor:BR_PLACEHOLDER,ra-names,64,12h
|
||||
dhcp-option=option6:dns-server,[2001:4860:4860::8888],[2606:4700:4700::1111]
|
||||
EOFV6
|
||||
printf '%s\n' \
|
||||
\"enable-ra\" \
|
||||
\"dhcp-range=::,constructor:BR_PLACEHOLDER,ra-names,64,12h\" \
|
||||
\"dhcp-option=option6:dns-server,[2001:4860:4860::8888],[2606:4700:4700::1111]\" >>\"$TMP\"
|
||||
sed -i \"s/BR_PLACEHOLDER/$BR/g\" \"$TMP\"
|
||||
fi
|
||||
|
||||
@@ -861,6 +957,11 @@ else
|
||||
rm -f \"$TMP\"
|
||||
systemctl enable --now dnsmasq || true
|
||||
fi
|
||||
|
||||
# Reload if main conf was updated to include conf-dir
|
||||
if [ \"$RELOAD\" = \"1\" ]; then
|
||||
systemctl reload dnsmasq || systemctl restart dnsmasq || true
|
||||
fi
|
||||
",
|
||||
br = shell_escape(bridge_name),
|
||||
br_addr = shell_escape(bridge_addr_cidr),
|
||||
@@ -872,6 +973,7 @@ fi
|
||||
|
||||
// Use a unique heredoc delimiter to avoid clashing with inner <<EOF blocks
|
||||
let heredoc_net = format!("bash -e -s <<'HERONET'\n{}\nHERONET\n", body);
|
||||
println!("executing command:\n{heredoc_net}");
|
||||
|
||||
match sal_process::run(&heredoc_net).silent(true).execute() {
|
||||
Ok(res) if res.success => Ok(()),
|
||||
|
@@ -4,6 +4,9 @@ use std::path::Path;
|
||||
|
||||
use sal_os;
|
||||
use sal_process;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::net::Ipv6Addr;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ImagePrepError {
|
||||
@@ -80,6 +83,19 @@ fn default_disable_cloud_init_net() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn stable_mac_from_id(id: &str) -> String {
|
||||
let mut h = DefaultHasher::new();
|
||||
id.hash(&mut h);
|
||||
let v = h.finish();
|
||||
let b0 = (((v >> 40) & 0xff) as u8 & 0xfe) | 0x02; // locally administered, unicast
|
||||
let b1 = ((v >> 32) & 0xff) as u8;
|
||||
let b2 = ((v >> 24) & 0xff) as u8;
|
||||
let b3 = ((v >> 16) & 0xff) as u8;
|
||||
let b4 = ((v >> 8) & 0xff) as u8;
|
||||
let b5 = (v & 0xff) as u8;
|
||||
format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", b0, b1, b2, b3, b4, b5)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ImagePrepResult {
|
||||
pub raw_disk: String,
|
||||
@@ -156,7 +172,67 @@ pub fn image_prepare(opts: &ImagePrepOptions) -> Result<ImagePrepResult, ImagePr
|
||||
// Build bash script that performs all steps and echos "RAW|ROOT_UUID|BOOT_UUID" at end
|
||||
let disable_ci_net = opts.disable_cloud_init_net;
|
||||
|
||||
// IPv6 static guest assignment (derive from mycelium interface) - enabled by default
|
||||
// If HERO_VIRT_IPV6_STATIC_GUEST=false, keep dynamic behavior (SLAAC/DHCPv6).
|
||||
let static_v6 = std::env::var("HERO_VIRT_IPV6_STATIC_GUEST")
|
||||
.map(|v| matches!(v.to_lowercase().as_str(), "" | "1" | "true" | "yes"))
|
||||
.unwrap_or(true);
|
||||
let myc_if = std::env::var("HERO_VIRT_MYCELIUM_IF").unwrap_or_else(|_| "mycelium".into());
|
||||
|
||||
// Discover host mycelium global IPv6 in 400::/7 from the interface
|
||||
let mut host_v6: Option<Ipv6Addr> = None;
|
||||
if static_v6 {
|
||||
let cmd = format!("ip -6 addr show dev {}", shell_escape(&myc_if));
|
||||
if let Ok(r) = sal_process::run(&cmd).silent(true).die(false).execute() {
|
||||
if r.success {
|
||||
for l in r.stdout.lines() {
|
||||
let lt = l.trim();
|
||||
if lt.starts_with("inet6 ") && lt.contains("scope global") {
|
||||
if let Some(addr_cidr) = lt.split_whitespace().nth(1) {
|
||||
let addr_only = addr_cidr.split('/').next().unwrap_or("").trim();
|
||||
if let Ok(ip) = addr_only.parse::<Ipv6Addr>() {
|
||||
let seg0 = ip.segments()[0];
|
||||
if (seg0 & 0xFE00) == 0x0400 {
|
||||
host_v6 = Some(ip);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Derive per-host /64 from mycelium and deterministic per-VM guest address
|
||||
let mut np_v6_block = String::new();
|
||||
let mut dhcp6_effective = opts.net.dhcp6;
|
||||
if static_v6 {
|
||||
if let Some(h) = host_v6 {
|
||||
let seg = h.segments();
|
||||
// Router = P::2; Guest address = P::<stable suffix>
|
||||
let mut hasher = DefaultHasher::new();
|
||||
opts.id.hash(&mut hasher);
|
||||
let mut suffix = (hasher.finish() as u16) & 0xfffe;
|
||||
if suffix == 0 || suffix == 2 {
|
||||
suffix = 0x100;
|
||||
}
|
||||
let guest_ip = Ipv6Addr::new(seg[0], seg[1], seg[2], seg[3], 0, 0, 0, suffix).to_string();
|
||||
let gw_ip = Ipv6Addr::new(seg[0], seg[1], seg[2], seg[3], 0, 0, 0, 2).to_string();
|
||||
|
||||
// Inject a YAML block for static v6
|
||||
np_v6_block = format!(
|
||||
" addresses:\n - {}/64\n routes:\n - to: \"::/0\"\n via: {}\n",
|
||||
guest_ip, gw_ip
|
||||
);
|
||||
// Disable dhcp6 when we provide a static address
|
||||
dhcp6_effective = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep script small and robust; avoid brace-heavy awk to simplify escaping.
|
||||
// Compute stable MAC (must match what vm_start() uses) and use it to match NIC in netplan.
|
||||
let vm_mac = stable_mac_from_id(&opts.id);
|
||||
let script = format!(
|
||||
"#!/bin/bash -e
|
||||
set -euo pipefail
|
||||
@@ -328,11 +404,69 @@ network:
|
||||
version: 2
|
||||
ethernets:
|
||||
eth0:
|
||||
match:
|
||||
macaddress: {vm_mac}
|
||||
set-name: eth0
|
||||
dhcp4: {dhcp4}
|
||||
dhcp6: {dhcp6}
|
||||
nameservers:
|
||||
{np_v6_block} nameservers:
|
||||
addresses: [8.8.8.8, 1.1.1.1, 2001:4860:4860::8888]
|
||||
EOF
|
||||
# Enable SSH password authentication and set a default password for 'ubuntu'
|
||||
mkdir -p \"$MNT_ROOT/etc/cloud/cloud.cfg.d\"
|
||||
printf '%s\n' 'ssh_pwauth: true' > \"$MNT_ROOT/etc/cloud/cloud.cfg.d/99-ssh-password-auth.cfg\"
|
||||
|
||||
mkdir -p \"$MNT_ROOT/etc/ssh/sshd_config.d\"
|
||||
cat > \"$MNT_ROOT/etc/ssh/sshd_config.d/99-hero-password-auth.conf\" << EOF
|
||||
PasswordAuthentication yes
|
||||
KbdInteractiveAuthentication yes
|
||||
UsePAM yes
|
||||
EOF
|
||||
|
||||
# Set password for default user 'ubuntu'
|
||||
if chroot \"$MNT_ROOT\" getent passwd ubuntu >/dev/null 2>&1; then
|
||||
chroot \"$MNT_ROOT\" bash -c \"echo 'ubuntu:ubuntu' | chpasswd\" || true
|
||||
fi
|
||||
# Ensure openssh-server is present (some cloud images may omit it)
|
||||
# Ensure SSH service enabled and keys generated on boot
|
||||
chroot \"$MNT_ROOT\" systemctl unmask ssh 2>/dev/null || true
|
||||
chroot \"$MNT_ROOT\" systemctl enable ssh 2>/dev/null || true
|
||||
chroot \"$MNT_ROOT\" systemctl enable ssh-keygen.service 2>/dev/null || true
|
||||
|
||||
# Ensure sshd listens on both IPv4 and IPv6 explicitly
|
||||
cat > \"$MNT_ROOT/etc/ssh/sshd_config.d/99-hero-address-family.conf\" << EOF
|
||||
AddressFamily any
|
||||
ListenAddress ::
|
||||
ListenAddress 0.0.0.0
|
||||
EOF
|
||||
|
||||
# If UFW is present, allow SSH and disable firewall (for tests)
|
||||
if chroot \"$MNT_ROOT\" command -v ufw >/dev/null 2>&1; then
|
||||
chroot \"$MNT_ROOT\" ufw allow OpenSSH || true
|
||||
chroot \"$MNT_ROOT\" ufw disable || true
|
||||
fi
|
||||
if ! chroot \"$MNT_ROOT\" test -x /usr/sbin/sshd; then
|
||||
cp -f /etc/resolv.conf \"$MNT_ROOT/etc/resolv.conf\" 2>/dev/null || true
|
||||
chroot \"$MNT_ROOT\" bash -c \"apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends openssh-server\" || true
|
||||
fi
|
||||
# Ensure default user 'ubuntu' exists (fallback for minimal images)
|
||||
if ! chroot \"$MNT_ROOT\" id -u ubuntu >/dev/null 2>&1; then
|
||||
chroot \"$MNT_ROOT\" useradd -m -s /bin/bash ubuntu || true
|
||||
echo \"ubuntu ALL=(ALL) NOPASSWD:ALL\" > \"$MNT_ROOT/etc/sudoers.d/90-ubuntu\" || true
|
||||
chmod 0440 \"$MNT_ROOT/etc/sudoers.d/90-ubuntu\" || true
|
||||
fi
|
||||
|
||||
# Re-assert password (covers both existing and newly created users)
|
||||
if chroot \"$MNT_ROOT\" getent passwd ubuntu >/dev/null 2>&1; then
|
||||
chroot \"$MNT_ROOT\" bash -c \"echo 'ubuntu:ubuntu' | chpasswd\" || true
|
||||
fi
|
||||
|
||||
# Pre-generate host SSH keys so sshd can start immediately
|
||||
chroot \"$MNT_ROOT\" ssh-keygen -A 2>/dev/null || true
|
||||
mkdir -p \"$MNT_ROOT/var/run/sshd\"
|
||||
|
||||
# Also enable socket activation as a fallback
|
||||
chroot \"$MNT_ROOT\" systemctl enable ssh.socket 2>/dev/null || true
|
||||
|
||||
# Disable cloud-init networking (optional but default)
|
||||
if [ \"{disable_ci_net}\" = \"true\" ]; then
|
||||
@@ -361,11 +495,16 @@ exit 0
|
||||
mnt_root = shell_escape(&mnt_root),
|
||||
mnt_boot = shell_escape(&mnt_boot),
|
||||
raw = shell_escape(&raw_path),
|
||||
vm_mac = vm_mac,
|
||||
dhcp4 = if opts.net.dhcp4 { "true" } else { "false" },
|
||||
dhcp6 = if opts.net.dhcp6 { "true" } else { "false" },
|
||||
dhcp6 = if dhcp6_effective { "true" } else { "false" },
|
||||
np_v6_block = np_v6_block,
|
||||
disable_ci_net = if disable_ci_net { "true" } else { "false" },
|
||||
);
|
||||
|
||||
// image prep script printout for debugging:
|
||||
println!("{script}");
|
||||
|
||||
let res = run_script(&script)?;
|
||||
// Prefer a RESULT:-prefixed line (robust against extra stdout noise)
|
||||
let mut marker: Option<String> = None;
|
||||
|
Reference in New Issue
Block a user