mirror of
https://github.com/CloudNebulaProject/zmgr.git
synced 2026-04-10 13:10:42 +00:00
Add multi-network zones and list-based IPAM pools
Templates now define named `net` blocks instead of a single pool reference, allowing zones like a router to attach to both internal and public networks. Pools support an `addresses` block with explicit IPs as an alternative to contiguous range-start/range-end — useful for hoster-assigned public addresses. Default init now includes a router template (internal + public) and a public pool with example addresses. Zone registry entries store per-net address/VNIC/stub/gateway. Import parses multiple net blocks from zonecfg info. Backward compatible with legacy single-pool templates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
50a6586f90
commit
430be11b13
8 changed files with 646 additions and 201 deletions
|
|
@ -20,11 +20,8 @@ The original VM scripts create ZFS volumes (`zfs create -V`). zmgr doesn't manag
|
||||||
### No Cloud-Init / Sysding Integration
|
### No Cloud-Init / Sysding Integration
|
||||||
The VM scripts generate cloud-init configs (user-data, meta-data, network-config). Zones don't use cloud-init but could benefit from sysding config generation for first-boot setup (hostname, SSH keys, networking).
|
The VM scripts generate cloud-init configs (user-data, meta-data, network-config). Zones don't use cloud-init but could benefit from sysding config generation for first-boot setup (hostname, SSH keys, networking).
|
||||||
|
|
||||||
### No Dry-Run Mode
|
|
||||||
`zmgr create --dry-run` could show what would happen without executing system commands. Useful for validation.
|
|
||||||
|
|
||||||
### No VNIC Naming Customization
|
### No VNIC Naming Customization
|
||||||
VNICs are always `<zonename>0`. Could support custom VNIC naming patterns.
|
VNICs are always `<zonename><index>`. Could support custom VNIC naming patterns per net.
|
||||||
|
|
||||||
### Import Matching is Best-Effort
|
### Import Matching is Best-Effort
|
||||||
Import matches zones to templates by brand and IPs to pools by network containment. Zones with unusual configs may get poor matches. Manual editing of the resulting KDL files may be needed.
|
Import matches zones to templates by brand and IPs to pools by network containment. Zones with unusual configs may get poor matches. Manual editing of the resulting KDL files may be needed.
|
||||||
|
|
@ -32,6 +29,12 @@ Import matches zones to templates by brand and IPs to pools by network containme
|
||||||
### No IPv6 Support
|
### No IPv6 Support
|
||||||
IPAM only handles IPv4 pools. Could extend to dual-stack.
|
IPAM only handles IPv4 pools. Could extend to dual-stack.
|
||||||
|
|
||||||
|
## Resolved
|
||||||
|
|
||||||
|
- ~~No Dry-Run Mode~~ — Implemented: `--dry-run` / `-n` flag on create and destroy
|
||||||
|
- ~~Single network per zone~~ — Implemented: templates define multiple `net` blocks, each referencing a pool
|
||||||
|
- ~~No public/hoster IP support~~ — Implemented: pools support explicit address lists in addition to contiguous ranges
|
||||||
|
|
||||||
## Future Considerations
|
## Future Considerations
|
||||||
|
|
||||||
- **Zone ordering**: Dependencies between zones (e.g., start DNS zone before app zones)
|
- **Zone ordering**: Dependencies between zones (e.g., start DNS zone before app zones)
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,22 @@
|
||||||
- [x] KDL parsing helpers (`kdl_util.rs`)
|
- [x] KDL parsing helpers (`kdl_util.rs`)
|
||||||
- [x] Error types with miette diagnostics (`error.rs`)
|
- [x] Error types with miette diagnostics (`error.rs`)
|
||||||
- [x] Global config loading (`config.rs`)
|
- [x] Global config loading (`config.rs`)
|
||||||
- [x] Template loading + defaults (`template.rs`)
|
- [x] Template loading + defaults — multi-net support (`template.rs`)
|
||||||
- [x] IPAM pool loading, allocation, defaults (`pool.rs`)
|
- [x] IPAM pool loading, allocation, defaults — range + list modes (`pool.rs`)
|
||||||
- [x] Zone registry CRUD (`zone.rs`)
|
- [x] Zone registry CRUD — multi-net support (`zone.rs`)
|
||||||
- [x] Publisher management (`publisher.rs`)
|
- [x] Publisher management (`publisher.rs`)
|
||||||
- [x] Exec layer for system commands (`exec.rs`)
|
- [x] Exec layer for system commands (`exec.rs`)
|
||||||
- [x] Import logic from existing zones (`import.rs`)
|
- [x] Import logic from existing zones — multi-net parsing (`import.rs`)
|
||||||
- [x] CLI commands: init, create, destroy, list, status, import
|
- [x] CLI commands: init, create, destroy, list, status, import
|
||||||
- [x] CLI commands: template list/show, pool list/show
|
- [x] CLI commands: template list/show, pool list/show
|
||||||
- [x] CLI commands: publisher list/add/remove
|
- [x] CLI commands: publisher list/add/remove
|
||||||
|
- [x] `--dry-run` / `-n` flag for create and destroy
|
||||||
|
- [x] Multi-network zones (multiple net blocks per template/zone)
|
||||||
|
- [x] List-based IPAM pools (explicit addresses from hoster)
|
||||||
|
- [x] Default router template with internal + public nets
|
||||||
|
- [x] Backward compat: legacy flat `pool` field in templates, flat address fields in zones
|
||||||
- [x] Clean build (zero warnings)
|
- [x] Clean build (zero warnings)
|
||||||
- [x] Tested: init, template list, pool list/show, publisher list, list
|
- [x] Tested: init, template list/show, pool list/show, publisher list, list, dry-run single+multi-net
|
||||||
|
|
||||||
## Not Yet Tested on illumos
|
## Not Yet Tested on illumos
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,30 @@ default-template "oi"
|
||||||
|
|
||||||
## templates/*.kdl
|
## templates/*.kdl
|
||||||
|
|
||||||
|
Templates define one or more `net` blocks, each referencing an IPAM pool.
|
||||||
|
|
||||||
```kdl
|
```kdl
|
||||||
|
// Single-net template
|
||||||
template "oi" {
|
template "oi" {
|
||||||
brand "ipkg"
|
brand "ipkg"
|
||||||
autoboot #false
|
autoboot #false
|
||||||
ip-type "exclusive"
|
ip-type "exclusive"
|
||||||
pool "internal"
|
net "internal" {
|
||||||
|
pool "internal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-net template (e.g., router with internal + public)
|
||||||
|
template "router" {
|
||||||
|
brand "ipkg"
|
||||||
|
autoboot #true
|
||||||
|
ip-type "exclusive"
|
||||||
|
net "internal" {
|
||||||
|
pool "internal"
|
||||||
|
}
|
||||||
|
net "public" {
|
||||||
|
pool "public"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -30,10 +48,16 @@ template "oi" {
|
||||||
| `brand` | string | yes | Zone brand (ipkg, nlipkg, etc.) |
|
| `brand` | string | yes | Zone brand (ipkg, nlipkg, etc.) |
|
||||||
| `autoboot` | bool | no | Boot zone after install (default: `#false`) |
|
| `autoboot` | bool | no | Boot zone after install (default: `#false`) |
|
||||||
| `ip-type` | string | no | IP type (default: `exclusive`) |
|
| `ip-type` | string | no | IP type (default: `exclusive`) |
|
||||||
| `pool` | string | yes | IPAM pool name to allocate from |
|
| `net "<name>"` | block | yes (1+) | Network attachment with pool reference |
|
||||||
|
|
||||||
|
Legacy: a flat `pool "name"` field is accepted as shorthand for a single net named "default".
|
||||||
|
|
||||||
## pools/*.kdl
|
## pools/*.kdl
|
||||||
|
|
||||||
|
Pools support two address source modes: **range** or **list**.
|
||||||
|
|
||||||
|
### Range-based pool (contiguous)
|
||||||
|
|
||||||
```kdl
|
```kdl
|
||||||
pool "internal" {
|
pool "internal" {
|
||||||
network "10.1.0.0/24"
|
network "10.1.0.0/24"
|
||||||
|
|
@ -44,28 +68,60 @@ pool "internal" {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### List-based pool (specific addresses)
|
||||||
|
|
||||||
|
For public IPs from a hoster where addresses may not be contiguous:
|
||||||
|
|
||||||
|
```kdl
|
||||||
|
pool "public" {
|
||||||
|
network "203.0.113.0/28"
|
||||||
|
gateway "203.0.113.1"
|
||||||
|
stub "pubstub0"
|
||||||
|
addresses {
|
||||||
|
address "203.0.113.2"
|
||||||
|
address "203.0.113.3"
|
||||||
|
address "203.0.113.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
| Field | Type | Required | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `network` | CIDR | yes | Network in CIDR notation |
|
| `network` | CIDR | yes | Network in CIDR notation |
|
||||||
| `gateway` | IPv4 | yes | Default router for zones in this pool |
|
| `gateway` | IPv4 | yes | Default router for zones in this pool |
|
||||||
| `stub` | string | yes | Etherstub/VNIC parent for zone VNICs |
|
| `stub` | string | yes | Etherstub/VNIC parent for zone VNICs |
|
||||||
| `range-start` | IPv4 | yes | First allocatable address |
|
| `range-start` | IPv4 | * | First allocatable address (range mode) |
|
||||||
| `range-end` | IPv4 | yes | Last allocatable address |
|
| `range-end` | IPv4 | * | Last allocatable address (range mode) |
|
||||||
|
| `addresses` | block | * | Explicit address list (list mode) |
|
||||||
|
|
||||||
|
\* Exactly one of range (`range-start` + `range-end`) or `addresses` is required.
|
||||||
|
|
||||||
## zones/*.kdl
|
## zones/*.kdl
|
||||||
|
|
||||||
|
Zone registry entries store per-net address/VNIC/stub/gateway:
|
||||||
|
|
||||||
```kdl
|
```kdl
|
||||||
zone "myzone" {
|
zone "gateway" {
|
||||||
template "oi"
|
template "router"
|
||||||
address "10.1.0.10/24"
|
|
||||||
gateway "10.1.0.1"
|
|
||||||
vnic "myzone0"
|
|
||||||
stub "oinetint0"
|
|
||||||
created "2026-03-22"
|
created "2026-03-22"
|
||||||
|
net "internal" {
|
||||||
|
address "10.1.0.10/24"
|
||||||
|
gateway "10.1.0.1"
|
||||||
|
vnic "gateway0"
|
||||||
|
stub "oinetint0"
|
||||||
|
}
|
||||||
|
net "public" {
|
||||||
|
address "203.0.113.2/28"
|
||||||
|
gateway "203.0.113.1"
|
||||||
|
vnic "gateway1"
|
||||||
|
stub "pubstub0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
These files are created by `zmgr create` and `zmgr import`. They serve as the IPAM allocation ledger — scanning all zone files determines which IPs are in use.
|
These files are created by `zmgr create` and `zmgr import`. They serve as the IPAM allocation ledger — scanning all zone net blocks determines which IPs are in use.
|
||||||
|
|
||||||
|
VNIC naming: `<zonename><index>` where index is the position in the net list (0, 1, 2...).
|
||||||
|
|
||||||
## publishers/*.kdl
|
## publishers/*.kdl
|
||||||
|
|
||||||
|
|
|
||||||
116
src/import.rs
116
src/import.rs
|
|
@ -4,7 +4,7 @@ use crate::error::Result;
|
||||||
use crate::exec;
|
use crate::exec;
|
||||||
use crate::pool::Pool;
|
use crate::pool::Pool;
|
||||||
use crate::template::Template;
|
use crate::template::Template;
|
||||||
use crate::zone::Zone;
|
use crate::zone::{Zone, ZoneNet};
|
||||||
|
|
||||||
/// Import existing zones from the system into the registry.
|
/// Import existing zones from the system into the registry.
|
||||||
///
|
///
|
||||||
|
|
@ -45,27 +45,16 @@ pub fn import_zones(registry: &Path, filter_name: Option<&str>) -> Result<Vec<St
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse zonecfg info output
|
// Parse all network interfaces from zonecfg info
|
||||||
let address = parse_zonecfg_field(&info, "allowed-address:")
|
let nets = parse_zonecfg_nets(&info, &entry.name, &pools, registry);
|
||||||
.unwrap_or_default();
|
|
||||||
let vnic = parse_zonecfg_field(&info, "physical:")
|
|
||||||
.unwrap_or_else(|| format!("{}0", entry.name));
|
|
||||||
let gateway = parse_zonecfg_field(&info, "defrouter:")
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// Match to a template by brand
|
// Match to a template by brand
|
||||||
let template = match_template(&entry.brand, &templates, registry);
|
let template = match_template(&entry.brand, &templates, registry);
|
||||||
|
|
||||||
// Match to a pool by finding which pool's network contains the address
|
|
||||||
let stub = match_pool_stub(&address, &pools, registry);
|
|
||||||
|
|
||||||
let zone = Zone {
|
let zone = Zone {
|
||||||
name: entry.name.clone(),
|
name: entry.name.clone(),
|
||||||
template: template.unwrap_or_else(|| entry.brand.clone()),
|
template: template.unwrap_or_else(|| entry.brand.clone()),
|
||||||
address,
|
nets,
|
||||||
gateway,
|
|
||||||
vnic,
|
|
||||||
stub: stub.unwrap_or_default(),
|
|
||||||
created: String::new(), // unknown for imported zones
|
created: String::new(), // unknown for imported zones
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -76,19 +65,100 @@ pub fn import_zones(registry: &Path, filter_name: Option<&str>) -> Result<Vec<St
|
||||||
Ok(imported)
|
Ok(imported)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a field from `zonecfg info` output.
|
/// Parse network interfaces from `zonecfg info` output.
|
||||||
/// Lines look like: `\tset allowed-address=10.1.0.5/24` or `\tallowed-address: 10.1.0.5/24`
|
///
|
||||||
fn parse_zonecfg_field(info: &str, field: &str) -> Option<String> {
|
/// zonecfg info outputs net blocks like:
|
||||||
|
/// ```text
|
||||||
|
/// net:
|
||||||
|
/// allowed-address: 10.1.0.5/24
|
||||||
|
/// physical: myzone0
|
||||||
|
/// defrouter: 10.1.0.1
|
||||||
|
/// ```
|
||||||
|
fn parse_zonecfg_nets(
|
||||||
|
info: &str,
|
||||||
|
zone_name: &str,
|
||||||
|
pool_names: &[String],
|
||||||
|
registry: &Path,
|
||||||
|
) -> Vec<ZoneNet> {
|
||||||
|
let mut nets = Vec::new();
|
||||||
|
let mut in_net = false;
|
||||||
|
let mut address = String::new();
|
||||||
|
let mut physical = String::new();
|
||||||
|
let mut defrouter = String::new();
|
||||||
|
|
||||||
for line in info.lines() {
|
for line in info.lines() {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
if let Some(rest) = trimmed.strip_prefix(field) {
|
|
||||||
let value = rest.trim().trim_matches('"');
|
if trimmed == "net:" {
|
||||||
if !value.is_empty() {
|
// Flush previous net if we had one
|
||||||
return Some(value.to_string());
|
if in_net && (!address.is_empty() || !physical.is_empty()) {
|
||||||
|
let stub = match_pool_stub(&address, pool_names, registry);
|
||||||
|
nets.push(ZoneNet {
|
||||||
|
name: format!("net{}", nets.len()),
|
||||||
|
address: address.clone(),
|
||||||
|
gateway: defrouter.clone(),
|
||||||
|
vnic: physical.clone(),
|
||||||
|
stub: stub.unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
in_net = true;
|
||||||
|
address.clear();
|
||||||
|
physical.clear();
|
||||||
|
defrouter.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A new non-net section ends the net block
|
||||||
|
if !trimmed.starts_with('[') && trimmed.ends_with(':') && trimmed != "net:" {
|
||||||
|
if in_net && (!address.is_empty() || !physical.is_empty()) {
|
||||||
|
let stub = match_pool_stub(&address, pool_names, registry);
|
||||||
|
nets.push(ZoneNet {
|
||||||
|
name: format!("net{}", nets.len()),
|
||||||
|
address: address.clone(),
|
||||||
|
gateway: defrouter.clone(),
|
||||||
|
vnic: physical.clone(),
|
||||||
|
stub: stub.unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
in_net = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_net {
|
||||||
|
if let Some(val) = trimmed.strip_prefix("allowed-address:") {
|
||||||
|
address = val.trim().trim_matches('"').to_string();
|
||||||
|
} else if let Some(val) = trimmed.strip_prefix("physical:") {
|
||||||
|
physical = val.trim().trim_matches('"').to_string();
|
||||||
|
} else if let Some(val) = trimmed.strip_prefix("defrouter:") {
|
||||||
|
defrouter = val.trim().trim_matches('"').to_string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
|
||||||
|
// Flush last net block
|
||||||
|
if in_net && (!address.is_empty() || !physical.is_empty()) {
|
||||||
|
let stub = match_pool_stub(&address, pool_names, registry);
|
||||||
|
nets.push(ZoneNet {
|
||||||
|
name: format!("net{}", nets.len()),
|
||||||
|
address,
|
||||||
|
gateway: defrouter,
|
||||||
|
vnic: physical,
|
||||||
|
stub: stub.unwrap_or_default(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no nets found, create a placeholder
|
||||||
|
if nets.is_empty() {
|
||||||
|
nets.push(ZoneNet {
|
||||||
|
name: "default".to_string(),
|
||||||
|
address: String::new(),
|
||||||
|
gateway: String::new(),
|
||||||
|
vnic: format!("{zone_name}0"),
|
||||||
|
stub: String::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
nets
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Match a brand to a template name.
|
/// Match a brand to a template name.
|
||||||
|
|
|
||||||
211
src/main.rs
211
src/main.rs
|
|
@ -23,7 +23,7 @@ use crate::error::{Result, ZmgrError};
|
||||||
use crate::pool::Pool;
|
use crate::pool::Pool;
|
||||||
use crate::publisher::Publisher;
|
use crate::publisher::Publisher;
|
||||||
use crate::template::Template;
|
use crate::template::Template;
|
||||||
use crate::zone::Zone;
|
use crate::zone::{Zone, ZoneNet};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "zmgr", about = "illumos zone manager")]
|
#[command(name = "zmgr", about = "illumos zone manager")]
|
||||||
|
|
@ -173,8 +173,8 @@ fn cmd_init(registry: &Path, force: bool) -> Result<()> {
|
||||||
|
|
||||||
println!("Initialized zmgr registry at {}", registry.display());
|
println!("Initialized zmgr registry at {}", registry.display());
|
||||||
println!(" config.kdl — global settings");
|
println!(" config.kdl — global settings");
|
||||||
println!(" templates/ — zone templates (oi, ofl)");
|
println!(" templates/ — zone templates (oi, ofl, router)");
|
||||||
println!(" pools/ — IPAM pools (internal, ofl)");
|
println!(" pools/ — IPAM pools (internal, ofl, public)");
|
||||||
println!(" publishers/ — IPS publishers (openindiana)");
|
println!(" publishers/ — IPS publishers (openindiana)");
|
||||||
println!(" zones/ — zone registry (empty)");
|
println!(" zones/ — zone registry (empty)");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -198,47 +198,76 @@ fn cmd_create(
|
||||||
let cfg = Config::load(registry)?;
|
let cfg = Config::load(registry)?;
|
||||||
let tmpl_name = template_name.unwrap_or(&cfg.default_template);
|
let tmpl_name = template_name.unwrap_or(&cfg.default_template);
|
||||||
let tmpl = Template::load(registry, tmpl_name)?;
|
let tmpl = Template::load(registry, tmpl_name)?;
|
||||||
let pool = Pool::load(registry, &tmpl.pool)?;
|
|
||||||
|
|
||||||
// Allocate next free IP
|
// Collect all allocated IPs across all zones
|
||||||
let zones = Zone::list(registry)?;
|
let zones = Zone::list(registry)?;
|
||||||
let ip = pool.allocate(&zones)?;
|
let used_ips = Zone::all_allocated_ips(&zones);
|
||||||
let prefix_len = pool.network.prefix_len();
|
|
||||||
let address = format!("{ip}/{prefix_len}");
|
// Resolve each network attachment: load pool, allocate IP, name VNIC
|
||||||
let vnic = format!("{name}0");
|
let mut zone_nets = Vec::new();
|
||||||
|
for (idx, tnet) in tmpl.nets.iter().enumerate() {
|
||||||
|
let pool = Pool::load(registry, &tnet.pool)?;
|
||||||
|
let ip = pool.allocate(&used_ips)?;
|
||||||
|
let prefix_len = pool.network.prefix_len();
|
||||||
|
let address = format!("{ip}/{prefix_len}");
|
||||||
|
let vnic = format!("{name}{idx}");
|
||||||
|
let gateway = pool.gateway.to_string();
|
||||||
|
let stub = pool.stub.clone();
|
||||||
|
|
||||||
|
zone_nets.push((tnet, pool, ZoneNet {
|
||||||
|
name: tnet.name.clone(),
|
||||||
|
address,
|
||||||
|
gateway,
|
||||||
|
vnic,
|
||||||
|
stub,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Build zonecfg commands
|
// Build zonecfg commands
|
||||||
let autoboot = if tmpl.autoboot { "true" } else { "false" };
|
let autoboot = if tmpl.autoboot { "true" } else { "false" };
|
||||||
let zonecfg_cmds = format!(
|
let mut zonecfg_cmds = format!(
|
||||||
"create -b\n\
|
"create -b\n\
|
||||||
set zonepath={}/{name}\n\
|
set zonepath={}/{name}\n\
|
||||||
set brand={}\n\
|
set brand={}\n\
|
||||||
set autoboot={autoboot}\n\
|
set autoboot={autoboot}\n\
|
||||||
set ip-type={}\n\
|
set ip-type={}\n",
|
||||||
add net\n\
|
cfg.zonepath_prefix, tmpl.brand, tmpl.ip_type
|
||||||
set physical={vnic}\n\
|
|
||||||
set allowed-address={address}\n\
|
|
||||||
set defrouter={}\n\
|
|
||||||
end\n\
|
|
||||||
verify\n\
|
|
||||||
commit\n",
|
|
||||||
cfg.zonepath_prefix, tmpl.brand, tmpl.ip_type, pool.gateway
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
for (_, _, znet) in &zone_nets {
|
||||||
|
zonecfg_cmds.push_str(&format!(
|
||||||
|
"add net\n\
|
||||||
|
set physical={}\n\
|
||||||
|
set allowed-address={}\n\
|
||||||
|
set defrouter={}\n\
|
||||||
|
end\n",
|
||||||
|
znet.vnic, znet.address, znet.gateway
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
zonecfg_cmds.push_str("verify\ncommit\n");
|
||||||
|
|
||||||
if dry_run {
|
if dry_run {
|
||||||
println!("[dry-run] Would create zone '{name}'");
|
println!("[dry-run] Would create zone '{name}'");
|
||||||
println!();
|
println!();
|
||||||
println!(" template: {tmpl_name}");
|
println!(" template: {tmpl_name}");
|
||||||
println!(" brand: {}", tmpl.brand);
|
println!(" brand: {}", tmpl.brand);
|
||||||
println!(" address: {address}");
|
|
||||||
println!(" vnic: {vnic} (on stub {})", pool.stub);
|
|
||||||
println!(" gateway: {}", pool.gateway);
|
|
||||||
println!(" zonepath: {}/{name}", cfg.zonepath_prefix);
|
println!(" zonepath: {}/{name}", cfg.zonepath_prefix);
|
||||||
println!(" autoboot: {}", tmpl.autoboot);
|
println!(" autoboot: {}", tmpl.autoboot);
|
||||||
|
println!(" networks: {}", zone_nets.len());
|
||||||
|
println!();
|
||||||
|
for (tnet, pool, znet) in &zone_nets {
|
||||||
|
println!(" net \"{}\" (pool: {})", tnet.name, tnet.pool);
|
||||||
|
println!(" address: {}", znet.address);
|
||||||
|
println!(" gateway: {}", znet.gateway);
|
||||||
|
println!(" vnic: {} (on stub {})", znet.vnic, pool.stub);
|
||||||
|
}
|
||||||
println!();
|
println!();
|
||||||
println!("Commands that would be executed:");
|
println!("Commands that would be executed:");
|
||||||
println!();
|
println!();
|
||||||
println!(" dladm create-vnic -l {} {vnic}", pool.stub);
|
for (_, pool, znet) in &zone_nets {
|
||||||
|
println!(" dladm create-vnic -l {} {}", pool.stub, znet.vnic);
|
||||||
|
}
|
||||||
println!();
|
println!();
|
||||||
println!(" zonecfg -z {name} <<EOF");
|
println!(" zonecfg -z {name} <<EOF");
|
||||||
for line in zonecfg_cmds.lines() {
|
for line in zonecfg_cmds.lines() {
|
||||||
|
|
@ -256,21 +285,24 @@ fn cmd_create(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create VNIC
|
// Create VNICs
|
||||||
exec::dladm_create_vnic(&vnic, &pool.stub)?;
|
for (_, _, znet) in &zone_nets {
|
||||||
|
exec::dladm_create_vnic(&znet.vnic, &znet.stub)?;
|
||||||
|
}
|
||||||
|
|
||||||
exec::zonecfg_create(name, &zonecfg_cmds)?;
|
exec::zonecfg_create(name, &zonecfg_cmds)?;
|
||||||
exec::zoneadm_install(name)?;
|
exec::zoneadm_install(name)?;
|
||||||
|
|
||||||
// Write registry entry
|
// Write registry entry
|
||||||
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
|
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
|
||||||
|
let nets: Vec<ZoneNet> = zone_nets
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, _, znet)| znet)
|
||||||
|
.collect();
|
||||||
let zone = Zone {
|
let zone = Zone {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
template: tmpl_name.to_string(),
|
template: tmpl_name.to_string(),
|
||||||
address: address.clone(),
|
nets,
|
||||||
gateway: pool.gateway.to_string(),
|
|
||||||
vnic: vnic.clone(),
|
|
||||||
stub: pool.stub.clone(),
|
|
||||||
created: today,
|
created: today,
|
||||||
};
|
};
|
||||||
zone.write(registry)?;
|
zone.write(registry)?;
|
||||||
|
|
@ -278,9 +310,9 @@ fn cmd_create(
|
||||||
println!("Created zone '{name}'");
|
println!("Created zone '{name}'");
|
||||||
println!(" template: {tmpl_name}");
|
println!(" template: {tmpl_name}");
|
||||||
println!(" brand: {}", tmpl.brand);
|
println!(" brand: {}", tmpl.brand);
|
||||||
println!(" address: {address}");
|
for net in &zone.nets {
|
||||||
println!(" vnic: {vnic}");
|
println!(" net \"{}\": {} via {} ({})", net.name, net.address, net.vnic, net.gateway);
|
||||||
println!(" gateway: {}", pool.gateway);
|
}
|
||||||
|
|
||||||
if tmpl.autoboot {
|
if tmpl.autoboot {
|
||||||
exec::zoneadm_boot(name)?;
|
exec::zoneadm_boot(name)?;
|
||||||
|
|
@ -302,15 +334,18 @@ fn cmd_destroy(registry: &Path, name: &str, dry_run: bool) -> Result<()> {
|
||||||
println!("[dry-run] Would destroy zone '{name}'");
|
println!("[dry-run] Would destroy zone '{name}'");
|
||||||
println!();
|
println!();
|
||||||
println!(" template: {}", zone.template);
|
println!(" template: {}", zone.template);
|
||||||
println!(" address: {}", zone.address);
|
for net in &zone.nets {
|
||||||
println!(" vnic: {}", zone.vnic);
|
println!(" net \"{}\": {} via {}", net.name, net.address, net.vnic);
|
||||||
|
}
|
||||||
println!();
|
println!();
|
||||||
println!("Commands that would be executed:");
|
println!("Commands that would be executed:");
|
||||||
println!();
|
println!();
|
||||||
println!(" zoneadm -z {name} halt");
|
println!(" zoneadm -z {name} halt");
|
||||||
println!(" zoneadm -z {name} uninstall -F");
|
println!(" zoneadm -z {name} uninstall -F");
|
||||||
println!(" zonecfg -z {name} delete -F");
|
println!(" zonecfg -z {name} delete -F");
|
||||||
println!(" dladm delete-vnic {}", zone.vnic);
|
for net in &zone.nets {
|
||||||
|
println!(" dladm delete-vnic {}", net.vnic);
|
||||||
|
}
|
||||||
println!();
|
println!();
|
||||||
println!("Registry entry that would be removed:");
|
println!("Registry entry that would be removed:");
|
||||||
println!(" {}/zones/{name}.kdl", registry.display());
|
println!(" {}/zones/{name}.kdl", registry.display());
|
||||||
|
|
@ -330,9 +365,11 @@ fn cmd_destroy(registry: &Path, name: &str, dry_run: bool) -> Result<()> {
|
||||||
eprintln!("warning: zonecfg delete: {e}");
|
eprintln!("warning: zonecfg delete: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete VNIC
|
// Delete all VNICs
|
||||||
if let Err(e) = exec::dladm_delete_vnic(&zone.vnic) {
|
for net in &zone.nets {
|
||||||
eprintln!("warning: dladm delete-vnic: {e}");
|
if let Err(e) = exec::dladm_delete_vnic(&net.vnic) {
|
||||||
|
eprintln!("warning: dladm delete-vnic {}: {e}", net.vnic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from registry
|
// Remove from registry
|
||||||
|
|
@ -352,13 +389,17 @@ fn cmd_list(registry: &Path) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{:<20} {:<10} {:<20} {:<15}",
|
"{:<20} {:<10} {:<10} {}",
|
||||||
"NAME", "TEMPLATE", "ADDRESS", "VNIC"
|
"NAME", "TEMPLATE", "NETS", "ADDRESSES"
|
||||||
);
|
);
|
||||||
for z in &zones {
|
for z in &zones {
|
||||||
|
let addrs: Vec<&str> = z.nets.iter().map(|n| n.address.as_str()).collect();
|
||||||
println!(
|
println!(
|
||||||
"{:<20} {:<10} {:<20} {:<15}",
|
"{:<20} {:<10} {:<10} {}",
|
||||||
z.name, z.template, z.address, z.vnic
|
z.name,
|
||||||
|
z.template,
|
||||||
|
z.nets.len(),
|
||||||
|
addrs.join(", ")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -380,16 +421,23 @@ fn cmd_status(registry: &Path, name: Option<&str>) -> Result<()> {
|
||||||
println!(" template: {}", r.template);
|
println!(" template: {}", r.template);
|
||||||
println!(" brand: {}", s.brand);
|
println!(" brand: {}", s.brand);
|
||||||
println!(" state: {}", s.state);
|
println!(" state: {}", s.state);
|
||||||
println!(" address: {}", r.address);
|
|
||||||
println!(" vnic: {}", r.vnic);
|
|
||||||
println!(" path: {}", s.path);
|
println!(" path: {}", s.path);
|
||||||
println!(" uuid: {}", s.uuid);
|
println!(" uuid: {}", s.uuid);
|
||||||
println!(" created: {}", r.created);
|
println!(" created: {}", r.created);
|
||||||
|
println!(" networks:");
|
||||||
|
for net in &r.nets {
|
||||||
|
println!(
|
||||||
|
" {:<12} {} via {} (gw {})",
|
||||||
|
net.name, net.address, net.vnic, net.gateway
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(Some(r), None) => {
|
(Some(r), None) => {
|
||||||
println!("Zone: {name} (in registry but not on system)");
|
println!("Zone: {name} (in registry but not on system)");
|
||||||
println!(" template: {}", r.template);
|
println!(" template: {}", r.template);
|
||||||
println!(" address: {}", r.address);
|
for net in &r.nets {
|
||||||
|
println!(" {:<12} {}", net.name, net.address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(None, Some(s)) => {
|
(None, Some(s)) => {
|
||||||
println!("Zone: {name} (on system but not in registry)");
|
println!("Zone: {name} (on system but not in registry)");
|
||||||
|
|
@ -406,8 +454,8 @@ fn cmd_status(registry: &Path, name: Option<&str>) -> Result<()> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println!(
|
println!(
|
||||||
"{:<20} {:<10} {:<12} {:<20} {:<10}",
|
"{:<20} {:<10} {:<12} {:<10} {}",
|
||||||
"NAME", "TEMPLATE", "STATE", "ADDRESS", "REGISTRY"
|
"NAME", "TEMPLATE", "STATE", "REGISTRY", "ADDRESSES"
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
|
|
@ -418,9 +466,10 @@ fn cmd_status(registry: &Path, name: Option<&str>) -> Result<()> {
|
||||||
.find(|s| s.name == r.name)
|
.find(|s| s.name == r.name)
|
||||||
.map(|s| s.state.as_str())
|
.map(|s| s.state.as_str())
|
||||||
.unwrap_or("absent");
|
.unwrap_or("absent");
|
||||||
|
let addrs: Vec<&str> = r.nets.iter().map(|n| n.address.as_str()).collect();
|
||||||
println!(
|
println!(
|
||||||
"{:<20} {:<10} {:<12} {:<20} {:<10}",
|
"{:<20} {:<10} {:<12} {:<10} {}",
|
||||||
r.name, r.template, state, r.address, "yes"
|
r.name, r.template, state, "yes", addrs.join(", ")
|
||||||
);
|
);
|
||||||
seen.insert(r.name.clone());
|
seen.insert(r.name.clone());
|
||||||
}
|
}
|
||||||
|
|
@ -428,8 +477,8 @@ fn cmd_status(registry: &Path, name: Option<&str>) -> Result<()> {
|
||||||
for s in &system_zones {
|
for s in &system_zones {
|
||||||
if !seen.contains(&s.name) && s.brand != "bhyve" {
|
if !seen.contains(&s.name) && s.brand != "bhyve" {
|
||||||
println!(
|
println!(
|
||||||
"{:<20} {:<10} {:<12} {:<20} {:<10}",
|
"{:<20} {:<10} {:<12} {:<10} {}",
|
||||||
s.name, s.brand, s.state, "-", "no"
|
s.name, s.brand, s.state, "no", "-"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -468,9 +517,17 @@ fn cmd_template_list(registry: &Path) -> Result<()> {
|
||||||
}
|
}
|
||||||
for name in &names {
|
for name in &names {
|
||||||
let tmpl = Template::load(registry, name)?;
|
let tmpl = Template::load(registry, name)?;
|
||||||
|
let net_desc: Vec<String> = tmpl
|
||||||
|
.nets
|
||||||
|
.iter()
|
||||||
|
.map(|n| format!("{}:{}", n.name, n.pool))
|
||||||
|
.collect();
|
||||||
println!(
|
println!(
|
||||||
"{:<10} brand={:<10} pool={:<10} autoboot={}",
|
"{:<10} brand={:<10} nets={:<25} autoboot={}",
|
||||||
name, tmpl.brand, tmpl.pool, tmpl.autoboot
|
name,
|
||||||
|
tmpl.brand,
|
||||||
|
net_desc.join(","),
|
||||||
|
tmpl.autoboot
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -484,7 +541,10 @@ fn cmd_template_show(registry: &Path, name: &str) -> Result<()> {
|
||||||
println!(" brand: {}", tmpl.brand);
|
println!(" brand: {}", tmpl.brand);
|
||||||
println!(" autoboot: {}", tmpl.autoboot);
|
println!(" autoboot: {}", tmpl.autoboot);
|
||||||
println!(" ip-type: {}", tmpl.ip_type);
|
println!(" ip-type: {}", tmpl.ip_type);
|
||||||
println!(" pool: {}", tmpl.pool);
|
println!(" networks:");
|
||||||
|
for net in &tmpl.nets {
|
||||||
|
println!(" {:<12} pool={}", net.name, net.pool);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -498,20 +558,15 @@ fn cmd_pool_list(registry: &Path) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let zones = Zone::list(registry)?;
|
let zones = Zone::list(registry)?;
|
||||||
|
let used_ips = Zone::all_allocated_ips(&zones);
|
||||||
|
|
||||||
for name in &names {
|
for name in &names {
|
||||||
let pool = Pool::load(registry, name)?;
|
let pool = Pool::load(registry, name)?;
|
||||||
let allocated: usize = zones
|
let allocated = used_ips
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|z| {
|
.filter(|ip| pool.network.contains(*ip))
|
||||||
z.address
|
|
||||||
.split('/')
|
|
||||||
.next()
|
|
||||||
.and_then(|ip| ip.parse::<Ipv4Addr>().ok())
|
|
||||||
.is_some_and(|ip| pool.network.contains(&ip))
|
|
||||||
})
|
|
||||||
.count();
|
.count();
|
||||||
let total = u32::from(pool.range_end) - u32::from(pool.range_start) + 1;
|
let total = pool.total_addresses();
|
||||||
println!(
|
println!(
|
||||||
"{:<10} network={:<18} stub={:<12} used={}/{}",
|
"{:<10} network={:<18} stub={:<12} used={}/{}",
|
||||||
name, pool.network, pool.stub, allocated, total
|
name, pool.network, pool.stub, allocated, total
|
||||||
|
|
@ -530,25 +585,31 @@ fn cmd_pool_show(registry: &Path, name: &str) -> Result<()> {
|
||||||
println!(" network: {}", pool.network);
|
println!(" network: {}", pool.network);
|
||||||
println!(" gateway: {}", pool.gateway);
|
println!(" gateway: {}", pool.gateway);
|
||||||
println!(" stub: {}", pool.stub);
|
println!(" stub: {}", pool.stub);
|
||||||
println!(" range: {} - {}", pool.range_start, pool.range_end);
|
println!(" addresses: {}", pool.source_description());
|
||||||
|
|
||||||
let allocated: Vec<&Zone> = zones
|
// Find allocations: zone nets whose IP falls in this pool
|
||||||
.iter()
|
let mut allocations = Vec::new();
|
||||||
.filter(|z| {
|
for z in &zones {
|
||||||
z.address
|
for net in &z.nets {
|
||||||
|
if let Some(ip) = net
|
||||||
|
.address
|
||||||
.split('/')
|
.split('/')
|
||||||
.next()
|
.next()
|
||||||
.and_then(|ip| ip.parse::<Ipv4Addr>().ok())
|
.and_then(|s| s.parse::<Ipv4Addr>().ok())
|
||||||
.is_some_and(|ip| pool.network.contains(&ip))
|
{
|
||||||
})
|
if pool.network.contains(&ip) {
|
||||||
.collect();
|
allocations.push((&z.name, &net.name, &net.address));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if allocated.is_empty() {
|
if allocations.is_empty() {
|
||||||
println!(" allocations: none");
|
println!(" allocations: none");
|
||||||
} else {
|
} else {
|
||||||
println!(" allocations:");
|
println!(" allocations:");
|
||||||
for z in &allocated {
|
for (zone_name, net_name, addr) in &allocations {
|
||||||
println!(" {} -> {}", z.address, z.name);
|
println!(" {addr} -> {zone_name} ({net_name})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
198
src/pool.rs
198
src/pool.rs
|
|
@ -5,7 +5,17 @@ use ipnet::Ipv4Net;
|
||||||
|
|
||||||
use crate::error::{Result, ZmgrError};
|
use crate::error::{Result, ZmgrError};
|
||||||
use crate::kdl_util;
|
use crate::kdl_util;
|
||||||
use crate::zone::Zone;
|
|
||||||
|
/// How addresses are defined in the pool.
|
||||||
|
pub enum AddressSource {
|
||||||
|
/// Contiguous range (range-start to range-end).
|
||||||
|
Range {
|
||||||
|
start: Ipv4Addr,
|
||||||
|
end: Ipv4Addr,
|
||||||
|
},
|
||||||
|
/// Explicit list of addresses.
|
||||||
|
List(Vec<Ipv4Addr>),
|
||||||
|
}
|
||||||
|
|
||||||
/// An IPAM address pool.
|
/// An IPAM address pool.
|
||||||
pub struct Pool {
|
pub struct Pool {
|
||||||
|
|
@ -13,8 +23,7 @@ pub struct Pool {
|
||||||
pub network: Ipv4Net,
|
pub network: Ipv4Net,
|
||||||
pub gateway: Ipv4Addr,
|
pub gateway: Ipv4Addr,
|
||||||
pub stub: String,
|
pub stub: String,
|
||||||
pub range_start: Ipv4Addr,
|
pub source: AddressSource,
|
||||||
pub range_end: Ipv4Addr,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pool {
|
impl Pool {
|
||||||
|
|
@ -65,72 +74,152 @@ impl Pool {
|
||||||
context: ctx.clone(),
|
context: ctx.clone(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let range_start_str = kdl_util::get_string(children, "range-start")
|
// Determine address source: explicit list or range
|
||||||
.ok_or_else(|| ZmgrError::MissingField {
|
let source = if let Some(addr_node) = children.get("addresses") {
|
||||||
field: "range-start".to_string(),
|
// Explicit address list
|
||||||
|
let addr_children = addr_node.children().ok_or_else(|| ZmgrError::MissingField {
|
||||||
|
field: "addresses children".to_string(),
|
||||||
context: ctx.clone(),
|
context: ctx.clone(),
|
||||||
})?;
|
})?;
|
||||||
let range_start: Ipv4Addr =
|
let mut addrs = Vec::new();
|
||||||
range_start_str
|
for node in addr_children.nodes() {
|
||||||
.parse()
|
if node.name().to_string() == "address" {
|
||||||
.map_err(|_| ZmgrError::MissingField {
|
let ip_str = node
|
||||||
field: format!("range-start (invalid IP: {range_start_str})"),
|
.entries()
|
||||||
|
.first()
|
||||||
|
.and_then(|e| e.value().as_string())
|
||||||
|
.ok_or_else(|| ZmgrError::MissingField {
|
||||||
|
field: "address value".to_string(),
|
||||||
|
context: ctx.clone(),
|
||||||
|
})?;
|
||||||
|
let ip: Ipv4Addr =
|
||||||
|
ip_str.parse().map_err(|_| ZmgrError::MissingField {
|
||||||
|
field: format!("address (invalid IP: {ip_str})"),
|
||||||
|
context: ctx.clone(),
|
||||||
|
})?;
|
||||||
|
addrs.push(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if addrs.is_empty() {
|
||||||
|
return Err(ZmgrError::MissingField {
|
||||||
|
field: "at least one address".to_string(),
|
||||||
|
context: ctx,
|
||||||
|
}
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
AddressSource::List(addrs)
|
||||||
|
} else {
|
||||||
|
// Range-based
|
||||||
|
let range_start_str = kdl_util::get_string(children, "range-start")
|
||||||
|
.ok_or_else(|| ZmgrError::MissingField {
|
||||||
|
field: "range-start (or addresses block)".to_string(),
|
||||||
context: ctx.clone(),
|
context: ctx.clone(),
|
||||||
})?;
|
})?;
|
||||||
|
let range_start: Ipv4Addr =
|
||||||
|
range_start_str
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ZmgrError::MissingField {
|
||||||
|
field: format!("range-start (invalid IP: {range_start_str})"),
|
||||||
|
context: ctx.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
let range_end_str = kdl_util::get_string(children, "range-end")
|
let range_end_str = kdl_util::get_string(children, "range-end")
|
||||||
.ok_or_else(|| ZmgrError::MissingField {
|
.ok_or_else(|| ZmgrError::MissingField {
|
||||||
field: "range-end".to_string(),
|
field: "range-end".to_string(),
|
||||||
context: ctx.clone(),
|
|
||||||
})?;
|
|
||||||
let range_end: Ipv4Addr =
|
|
||||||
range_end_str
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| ZmgrError::MissingField {
|
|
||||||
field: format!("range-end (invalid IP: {range_end_str})"),
|
|
||||||
context: ctx.clone(),
|
context: ctx.clone(),
|
||||||
})?;
|
})?;
|
||||||
|
let range_end: Ipv4Addr =
|
||||||
|
range_end_str
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| ZmgrError::MissingField {
|
||||||
|
field: format!("range-end (invalid IP: {range_end_str})"),
|
||||||
|
context: ctx.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
AddressSource::Range {
|
||||||
|
start: range_start,
|
||||||
|
end: range_end,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Pool {
|
Ok(Pool {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
network,
|
network,
|
||||||
gateway,
|
gateway,
|
||||||
stub,
|
stub,
|
||||||
range_start,
|
source,
|
||||||
range_end,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allocate the next free IP from this pool, given existing zones.
|
/// Allocate the next free IP from this pool.
|
||||||
pub fn allocate(&self, zones: &[Zone]) -> Result<Ipv4Addr> {
|
/// `used` is the set of IPs already allocated across all zones.
|
||||||
let allocated: Vec<Ipv4Addr> = zones
|
pub fn allocate(&self, used: &[Ipv4Addr]) -> Result<Ipv4Addr> {
|
||||||
|
let in_pool: Vec<Ipv4Addr> = used
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|z| {
|
.copied()
|
||||||
// address is stored as "x.x.x.x/prefix"
|
|
||||||
z.address
|
|
||||||
.split('/')
|
|
||||||
.next()
|
|
||||||
.and_then(|ip| ip.parse::<Ipv4Addr>().ok())
|
|
||||||
})
|
|
||||||
.filter(|ip| self.network.contains(ip))
|
.filter(|ip| self.network.contains(ip))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let start = u32::from(self.range_start);
|
match &self.source {
|
||||||
let end = u32::from(self.range_end);
|
AddressSource::Range { start, end } => {
|
||||||
|
let s = u32::from(*start);
|
||||||
for candidate in start..=end {
|
let e = u32::from(*end);
|
||||||
let ip = Ipv4Addr::from(candidate);
|
for candidate in s..=e {
|
||||||
if !allocated.contains(&ip) {
|
let ip = Ipv4Addr::from(candidate);
|
||||||
return Ok(ip);
|
if !in_pool.contains(&ip) {
|
||||||
|
return Ok(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(ZmgrError::PoolExhausted {
|
||||||
|
pool: self.name.clone(),
|
||||||
|
range_start: start.to_string(),
|
||||||
|
range_end: end.to_string(),
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
AddressSource::List(addrs) => {
|
||||||
|
for addr in addrs {
|
||||||
|
if !in_pool.contains(addr) {
|
||||||
|
return Ok(*addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let first = addrs.first().unwrap();
|
||||||
|
let last = addrs.last().unwrap();
|
||||||
|
Err(ZmgrError::PoolExhausted {
|
||||||
|
pool: self.name.clone(),
|
||||||
|
range_start: first.to_string(),
|
||||||
|
range_end: last.to_string(),
|
||||||
|
}
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Err(ZmgrError::PoolExhausted {
|
/// Total number of allocatable addresses.
|
||||||
pool: self.name.clone(),
|
pub fn total_addresses(&self) -> u32 {
|
||||||
range_start: self.range_start.to_string(),
|
match &self.source {
|
||||||
range_end: self.range_end.to_string(),
|
AddressSource::Range { start, end } => {
|
||||||
|
u32::from(*end) - u32::from(*start) + 1
|
||||||
|
}
|
||||||
|
AddressSource::List(addrs) => addrs.len() as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Human-readable description of the address source.
|
||||||
|
pub fn source_description(&self) -> String {
|
||||||
|
match &self.source {
|
||||||
|
AddressSource::Range { start, end } => {
|
||||||
|
format!("{start} - {end}")
|
||||||
|
}
|
||||||
|
AddressSource::List(addrs) => {
|
||||||
|
if addrs.len() <= 5 {
|
||||||
|
let strs: Vec<String> = addrs.iter().map(|a| a.to_string()).collect();
|
||||||
|
strs.join(", ")
|
||||||
|
} else {
|
||||||
|
format!("{} addresses", addrs.len())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List all pool names.
|
/// List all pool names.
|
||||||
|
|
@ -184,6 +273,25 @@ impl Pool {
|
||||||
)
|
)
|
||||||
.map_err(ZmgrError::Io)?;
|
.map_err(ZmgrError::Io)?;
|
||||||
|
|
||||||
|
// Example public pool with explicit address list
|
||||||
|
std::fs::write(
|
||||||
|
dir.join("public.kdl"),
|
||||||
|
r#"// Public addresses from hoster — list each usable IP
|
||||||
|
pool "public" {
|
||||||
|
network "203.0.113.0/28"
|
||||||
|
gateway "203.0.113.1"
|
||||||
|
stub "pubstub0"
|
||||||
|
addresses {
|
||||||
|
address "203.0.113.2"
|
||||||
|
address "203.0.113.3"
|
||||||
|
address "203.0.113.4"
|
||||||
|
address "203.0.113.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.map_err(ZmgrError::Io)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,14 @@ use std::path::Path;
|
||||||
use crate::error::{Result, ZmgrError};
|
use crate::error::{Result, ZmgrError};
|
||||||
use crate::kdl_util;
|
use crate::kdl_util;
|
||||||
|
|
||||||
|
/// A network attachment in a template.
|
||||||
|
pub struct TemplateNet {
|
||||||
|
/// Logical name for this network (used in zone registry and VNIC naming).
|
||||||
|
pub name: String,
|
||||||
|
/// IPAM pool to allocate from.
|
||||||
|
pub pool: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// A zone creation template.
|
/// A zone creation template.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct Template {
|
pub struct Template {
|
||||||
|
|
@ -10,7 +18,7 @@ pub struct Template {
|
||||||
pub brand: String,
|
pub brand: String,
|
||||||
pub autoboot: bool,
|
pub autoboot: bool,
|
||||||
pub ip_type: String,
|
pub ip_type: String,
|
||||||
pub pool: String,
|
pub nets: Vec<TemplateNet>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Template {
|
impl Template {
|
||||||
|
|
@ -26,7 +34,6 @@ impl Template {
|
||||||
let doc = kdl_util::read_document(&path)?;
|
let doc = kdl_util::read_document(&path)?;
|
||||||
let ctx = format!("template '{name}'");
|
let ctx = format!("template '{name}'");
|
||||||
|
|
||||||
// The template node wraps the children
|
|
||||||
let template_node = doc.get("template").ok_or_else(|| ZmgrError::MissingField {
|
let template_node = doc.get("template").ok_or_else(|| ZmgrError::MissingField {
|
||||||
field: "template".to_string(),
|
field: "template".to_string(),
|
||||||
context: ctx.clone(),
|
context: ctx.clone(),
|
||||||
|
|
@ -47,18 +54,62 @@ impl Template {
|
||||||
let ip_type = kdl_util::get_string(children, "ip-type")
|
let ip_type = kdl_util::get_string(children, "ip-type")
|
||||||
.unwrap_or_else(|| "exclusive".to_string());
|
.unwrap_or_else(|| "exclusive".to_string());
|
||||||
|
|
||||||
let pool = kdl_util::get_string(children, "pool")
|
// Parse network attachments: either `net` blocks or legacy `pool` field
|
||||||
.ok_or_else(|| ZmgrError::MissingField {
|
let mut nets = Vec::new();
|
||||||
field: "pool".to_string(),
|
for node in children.nodes() {
|
||||||
context: ctx.clone(),
|
if node.name().to_string() == "net" {
|
||||||
})?;
|
let net_name = node
|
||||||
|
.entries()
|
||||||
|
.first()
|
||||||
|
.and_then(|e| e.value().as_string())
|
||||||
|
.ok_or_else(|| ZmgrError::MissingField {
|
||||||
|
field: "net name argument".to_string(),
|
||||||
|
context: ctx.clone(),
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let net_children = node.children().ok_or_else(|| ZmgrError::MissingField {
|
||||||
|
field: format!("net '{net_name}' children"),
|
||||||
|
context: ctx.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let pool = kdl_util::get_string(net_children, "pool")
|
||||||
|
.ok_or_else(|| ZmgrError::MissingField {
|
||||||
|
field: format!("pool in net '{net_name}'"),
|
||||||
|
context: ctx.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
nets.push(TemplateNet {
|
||||||
|
name: net_name,
|
||||||
|
pool,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward compat: if no `net` blocks, look for a single `pool` field
|
||||||
|
if nets.is_empty() {
|
||||||
|
if let Some(pool) = kdl_util::get_string(children, "pool") {
|
||||||
|
nets.push(TemplateNet {
|
||||||
|
name: "default".to_string(),
|
||||||
|
pool,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nets.is_empty() {
|
||||||
|
return Err(ZmgrError::MissingField {
|
||||||
|
field: "at least one net block (or pool field)".to_string(),
|
||||||
|
context: ctx,
|
||||||
|
}
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Template {
|
Ok(Template {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
brand,
|
brand,
|
||||||
autoboot,
|
autoboot,
|
||||||
ip_type,
|
ip_type,
|
||||||
pool,
|
nets,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,13 +138,16 @@ impl Template {
|
||||||
let dir = registry.join("templates");
|
let dir = registry.join("templates");
|
||||||
std::fs::create_dir_all(&dir).map_err(ZmgrError::Io)?;
|
std::fs::create_dir_all(&dir).map_err(ZmgrError::Io)?;
|
||||||
|
|
||||||
|
// Single-net templates (backward compatible with pool field)
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
dir.join("oi.kdl"),
|
dir.join("oi.kdl"),
|
||||||
r#"template "oi" {
|
r#"template "oi" {
|
||||||
brand "ipkg"
|
brand "ipkg"
|
||||||
autoboot #false
|
autoboot #false
|
||||||
ip-type "exclusive"
|
ip-type "exclusive"
|
||||||
pool "internal"
|
net "internal" {
|
||||||
|
pool "internal"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
|
|
@ -105,7 +159,27 @@ impl Template {
|
||||||
brand "nlipkg"
|
brand "nlipkg"
|
||||||
autoboot #true
|
autoboot #true
|
||||||
ip-type "exclusive"
|
ip-type "exclusive"
|
||||||
pool "ofl"
|
net "ofl" {
|
||||||
|
pool "ofl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.map_err(ZmgrError::Io)?;
|
||||||
|
|
||||||
|
// Multi-net template example: router with internal + public
|
||||||
|
std::fs::write(
|
||||||
|
dir.join("router.kdl"),
|
||||||
|
r#"template "router" {
|
||||||
|
brand "ipkg"
|
||||||
|
autoboot #true
|
||||||
|
ip-type "exclusive"
|
||||||
|
net "internal" {
|
||||||
|
pool "internal"
|
||||||
|
}
|
||||||
|
net "public" {
|
||||||
|
pool "public"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
124
src/zone.rs
124
src/zone.rs
|
|
@ -1,16 +1,24 @@
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::error::{Result, ZmgrError};
|
use crate::error::{Result, ZmgrError};
|
||||||
use crate::kdl_util;
|
use crate::kdl_util;
|
||||||
|
|
||||||
/// A managed zone registry entry.
|
/// A network attachment in a zone registry entry.
|
||||||
pub struct Zone {
|
pub struct ZoneNet {
|
||||||
|
/// Logical name (matches the template net name).
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub template: String,
|
|
||||||
pub address: String,
|
pub address: String,
|
||||||
pub gateway: String,
|
pub gateway: String,
|
||||||
pub vnic: String,
|
pub vnic: String,
|
||||||
pub stub: String,
|
pub stub: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A managed zone registry entry.
|
||||||
|
pub struct Zone {
|
||||||
|
pub name: String,
|
||||||
|
pub template: String,
|
||||||
|
pub nets: Vec<ZoneNet>,
|
||||||
pub created: String,
|
pub created: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,20 +59,64 @@ impl Zone {
|
||||||
context: ctx.clone(),
|
context: ctx.clone(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let template = kdl_util::get_string(children, "template").unwrap_or_default();
|
||||||
|
let created = kdl_util::get_string(children, "created").unwrap_or_default();
|
||||||
|
|
||||||
|
// Parse net blocks
|
||||||
|
let mut nets = Vec::new();
|
||||||
|
for node in children.nodes() {
|
||||||
|
if node.name().to_string() == "net" {
|
||||||
|
let net_name = node
|
||||||
|
.entries()
|
||||||
|
.first()
|
||||||
|
.and_then(|e| e.value().as_string())
|
||||||
|
.unwrap_or("default")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let net_children = node.children();
|
||||||
|
let (address, gateway, vnic, stub) = if let Some(nc) = net_children {
|
||||||
|
(
|
||||||
|
kdl_util::get_string(nc, "address").unwrap_or_default(),
|
||||||
|
kdl_util::get_string(nc, "gateway").unwrap_or_default(),
|
||||||
|
kdl_util::get_string(nc, "vnic").unwrap_or_default(),
|
||||||
|
kdl_util::get_string(nc, "stub").unwrap_or_default(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
nets.push(ZoneNet {
|
||||||
|
name: net_name,
|
||||||
|
address,
|
||||||
|
gateway,
|
||||||
|
vnic,
|
||||||
|
stub,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward compat: flat address/vnic/stub/gateway fields (single net)
|
||||||
|
if nets.is_empty() {
|
||||||
|
let address = kdl_util::get_string(children, "address").unwrap_or_default();
|
||||||
|
let gateway = kdl_util::get_string(children, "gateway").unwrap_or_default();
|
||||||
|
let vnic = kdl_util::get_string(children, "vnic").unwrap_or_default();
|
||||||
|
let stub = kdl_util::get_string(children, "stub").unwrap_or_default();
|
||||||
|
if !address.is_empty() || !vnic.is_empty() {
|
||||||
|
nets.push(ZoneNet {
|
||||||
|
name: "default".to_string(),
|
||||||
|
address,
|
||||||
|
gateway,
|
||||||
|
vnic,
|
||||||
|
stub,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Zone {
|
Ok(Zone {
|
||||||
name: name.clone(),
|
name,
|
||||||
template: kdl_util::get_string(children, "template")
|
template,
|
||||||
.unwrap_or_default(),
|
nets,
|
||||||
address: kdl_util::get_string(children, "address")
|
created,
|
||||||
.unwrap_or_default(),
|
|
||||||
gateway: kdl_util::get_string(children, "gateway")
|
|
||||||
.unwrap_or_default(),
|
|
||||||
vnic: kdl_util::get_string(children, "vnic")
|
|
||||||
.unwrap_or_default(),
|
|
||||||
stub: kdl_util::get_string(children, "stub")
|
|
||||||
.unwrap_or_default(),
|
|
||||||
created: kdl_util::get_string(children, "created")
|
|
||||||
.unwrap_or_default(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,18 +125,20 @@ impl Zone {
|
||||||
let dir = registry.join("zones");
|
let dir = registry.join("zones");
|
||||||
std::fs::create_dir_all(&dir).map_err(ZmgrError::Io)?;
|
std::fs::create_dir_all(&dir).map_err(ZmgrError::Io)?;
|
||||||
let path = dir.join(format!("{}.kdl", self.name));
|
let path = dir.join(format!("{}.kdl", self.name));
|
||||||
let content = format!(
|
|
||||||
r#"zone "{}" {{
|
let mut content = format!("zone \"{}\" {{\n", self.name);
|
||||||
template "{}"
|
content.push_str(&format!(" template \"{}\"\n", self.template));
|
||||||
address "{}"
|
content.push_str(&format!(" created \"{}\"\n", self.created));
|
||||||
gateway "{}"
|
for net in &self.nets {
|
||||||
vnic "{}"
|
content.push_str(&format!(" net \"{}\" {{\n", net.name));
|
||||||
stub "{}"
|
content.push_str(&format!(" address \"{}\"\n", net.address));
|
||||||
created "{}"
|
content.push_str(&format!(" gateway \"{}\"\n", net.gateway));
|
||||||
}}
|
content.push_str(&format!(" vnic \"{}\"\n", net.vnic));
|
||||||
"#,
|
content.push_str(&format!(" stub \"{}\"\n", net.stub));
|
||||||
self.name, self.template, self.address, self.gateway, self.vnic, self.stub, self.created
|
content.push_str(" }\n");
|
||||||
);
|
}
|
||||||
|
content.push_str("}\n");
|
||||||
|
|
||||||
std::fs::write(&path, content).map_err(ZmgrError::Io)?;
|
std::fs::write(&path, content).map_err(ZmgrError::Io)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -121,6 +175,20 @@ impl Zone {
|
||||||
Ok(zones)
|
Ok(zones)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collect all allocated IPs across all zones' nets.
|
||||||
|
pub fn all_allocated_ips(zones: &[Zone]) -> Vec<Ipv4Addr> {
|
||||||
|
zones
|
||||||
|
.iter()
|
||||||
|
.flat_map(|z| &z.nets)
|
||||||
|
.filter_map(|net| {
|
||||||
|
net.address
|
||||||
|
.split('/')
|
||||||
|
.next()
|
||||||
|
.and_then(|ip| ip.parse::<Ipv4Addr>().ok())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if a zone exists in the registry.
|
/// Check if a zone exists in the registry.
|
||||||
pub fn exists(registry: &Path, name: &str) -> bool {
|
pub fn exists(registry: &Path, name: &str) -> bool {
|
||||||
registry
|
registry
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue