use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SchemaVersion {
#[serde(rename = "Major")]
pub major: u32,
#[serde(rename = "Minor")]
pub minor: u32,
}
impl Default for SchemaVersion {
fn default() -> Self {
Self { major: 2, minor: 0 }
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum NetworkType {
#[serde(rename = "NAT")]
#[default]
Nat,
#[serde(rename = "Transparent")]
Transparent,
#[serde(rename = "L2Bridge")]
L2Bridge,
#[serde(rename = "L2Tunnel")]
L2Tunnel,
#[serde(rename = "Overlay")]
Overlay,
#[serde(rename = "Internal")]
Internal,
#[serde(rename = "Private")]
Private,
#[serde(rename = "ICS")]
Ics,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct HostComputeNetwork {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>, pub name: String,
#[serde(rename = "Type")]
pub ty: NetworkType,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub policies: Vec<serde_json::Value>, #[serde(default, skip_serializing_if = "Option::is_none")]
pub mac_pool: Option<MacPool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dns: Option<Dns>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ipams: Vec<Ipam>,
#[serde(default)]
pub flags: u32,
pub schema_version: SchemaVersion,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct MacPool {
pub ranges: Vec<MacRange>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct MacRange {
pub start_mac_address: String,
pub end_mac_address: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Dns {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub domain: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub search: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub server_list: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub options: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Ipam {
#[serde(rename = "Type", default = "default_ipam_type")]
pub ty: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub subnets: Vec<Subnet>,
}
fn default_ipam_type() -> String {
"Static".to_string()
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Subnet {
pub ip_address_prefix: String, #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub routes: Vec<Route>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub policies: Vec<serde_json::Value>, }
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Route {
pub next_hop: String,
pub destination_prefix: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metric: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct HostComputeEndpoint {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub name: String,
pub host_compute_network: String, #[serde(default, skip_serializing_if = "Option::is_none")]
pub host_compute_namespace: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub policies: Vec<serde_json::Value>,
#[serde(
rename = "IPConfigurations",
default,
skip_serializing_if = "Vec::is_empty"
)]
pub ip_configurations: Vec<IpConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dns: Option<Dns>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub routes: Vec<Route>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub mac_address: String,
#[serde(default)]
pub flags: u32,
pub schema_version: SchemaVersion,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct IpConfig {
pub ip_address: String,
pub prefix_length: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct EndpointPolicy {
#[serde(rename = "Type")]
pub ty: EndpointPolicyType,
pub settings: serde_json::Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EndpointPolicyType {
#[serde(rename = "PortMapping")]
PortMapping,
#[serde(rename = "ACL")]
Acl,
#[serde(rename = "QOS")]
Qos,
#[serde(rename = "OutBoundNAT")]
OutBoundNat,
#[serde(rename = "SDNRoute")]
SdnRoute,
#[serde(rename = "L4Proxy")]
L4Proxy,
#[serde(rename = "L4WFPPROXY")]
L4WfpProxy,
#[serde(rename = "PortName")]
PortName,
#[serde(rename = "EncapOverhead")]
EncapOverhead,
#[serde(rename = "InterfaceConstraint")]
InterfaceConstraint,
#[serde(rename = "ProviderAddress")]
ProviderAddress,
#[serde(rename = "Iov")]
Iov,
#[serde(rename = "TierAcl")]
TierAcl,
#[serde(rename = "NetworkACL")]
NetworkAcl,
#[serde(rename = "NetworkL4Proxy")]
NetworkL4Proxy,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct OutBoundNatPolicySetting {
#[serde(
default,
skip_serializing_if = "String::is_empty",
rename = "VirtualIP"
)]
pub virtual_ip: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exceptions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub destinations: Vec<String>,
#[serde(default, skip_serializing_if = "u32_is_zero")]
pub flags: u32,
#[serde(default, skip_serializing_if = "u16_is_zero")]
pub max_port_pool_usage: u16,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct SdnRoutePolicySetting {
pub destination_prefix: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub next_hop: String,
#[serde(default)]
pub need_encap: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct AclPolicySetting {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub protocols: String,
pub action: AclAction,
pub direction: AclDirection,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub local_addresses: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub remote_addresses: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub local_ports: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub remote_ports: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub rule_type: String,
#[serde(default, skip_serializing_if = "u16_is_zero")]
pub priority: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AclAction {
Allow,
Block,
Pass,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AclDirection {
In,
Out,
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn u16_is_zero(v: &u16) -> bool {
*v == 0
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn u32_is_zero(v: &u32) -> bool {
*v == 0
}
impl EndpointPolicy {
#[must_use]
pub fn out_bound_nat(exceptions: Vec<String>) -> Self {
let settings = OutBoundNatPolicySetting {
exceptions,
..OutBoundNatPolicySetting::default()
};
Self {
ty: EndpointPolicyType::OutBoundNat,
settings: serde_json::to_value(settings)
.expect("OutBoundNatPolicySetting is plain data, cannot fail"),
}
}
#[must_use]
pub fn sdn_route(destination_prefix: impl Into<String>, need_encap: bool) -> Self {
let settings = SdnRoutePolicySetting {
destination_prefix: destination_prefix.into(),
need_encap,
..SdnRoutePolicySetting::default()
};
Self {
ty: EndpointPolicyType::SdnRoute,
settings: serde_json::to_value(settings)
.expect("SdnRoutePolicySetting is plain data, cannot fail"),
}
}
#[must_use]
pub fn acl_in_allow(remote_addresses: impl Into<String>) -> Self {
let settings = AclPolicySetting {
protocols: String::new(),
action: AclAction::Allow,
direction: AclDirection::In,
local_addresses: String::new(),
remote_addresses: remote_addresses.into(),
local_ports: String::new(),
remote_ports: String::new(),
rule_type: String::new(),
priority: 0,
};
Self {
ty: EndpointPolicyType::Acl,
settings: serde_json::to_value(settings)
.expect("AclPolicySetting is plain data, cannot fail"),
}
}
}
impl From<EndpointPolicy> for serde_json::Value {
fn from(policy: EndpointPolicy) -> Self {
serde_json::to_value(policy).expect("EndpointPolicy is plain data, cannot fail")
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum NamespaceType {
#[serde(rename = "HostDefault")]
#[default]
HostDefault,
#[serde(rename = "HostInterface")]
HostInterface,
#[serde(rename = "Guest")]
Guest,
#[serde(rename = "GuestInterface")]
GuestInterface,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct HostComputeNamespace {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default)]
pub namespace_id: u32, #[serde(rename = "Type", default)]
pub ty: NamespaceType,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub resources: Vec<NamespaceResource>,
pub schema_version: SchemaVersion,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct NamespaceResource {
#[serde(rename = "Type")]
pub ty: String, #[serde(default)]
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct EndpointStats {
pub endpoint_id: String,
pub instance_id: String,
pub bytes_received: u64,
pub bytes_sent: u64,
pub packets_received: u64,
pub packets_sent: u64,
pub dropped_packets_incoming: u64,
pub dropped_packets_outgoing: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ModifyRequestType {
Add,
Remove,
Update,
Refresh,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ModifyNamespaceSettingRequest {
pub resource_type: u32,
pub request_type: ModifyRequestType,
pub settings: serde_json::Value, }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ModifyEndpointSettingRequest {
#[serde(rename = "ResourceType")]
pub resource_type: String, pub request_type: ModifyRequestType,
pub settings: serde_json::Value,
}
#[cfg(test)]
mod tests {
use super::{
Dns, EndpointStats, HostComputeEndpoint, HostComputeNetwork, IpConfig, Ipam,
ModifyNamespaceSettingRequest, ModifyRequestType, NetworkType, Route, SchemaVersion,
Subnet,
};
use serde_json::json;
#[test]
fn schema_version_default_is_v2_0() {
let v = SchemaVersion::default();
assert_eq!(v.major, 2);
assert_eq!(v.minor, 0);
}
#[test]
fn endpoint_json_preserves_two_cap_ip_configurations() {
let ep = HostComputeEndpoint {
name: "ep1".to_string(),
host_compute_network: "net-guid".to_string(),
ip_configurations: vec![IpConfig {
ip_address: "10.0.0.5".to_string(),
prefix_length: 24,
}],
..HostComputeEndpoint::default()
};
let s = serde_json::to_string(&ep).unwrap();
assert!(
s.contains("\"IPConfigurations\":["),
"expected two-cap IPConfigurations in JSON: {s}"
);
assert!(
!s.contains("\"IpConfigurations\""),
"must not emit one-cap IpConfigurations: {s}"
);
assert!(
s.contains("\"IpAddress\":\"10.0.0.5\""),
"expected one-cap IpAddress in JSON: {s}"
);
}
#[test]
fn ipam_static_default_type() {
let raw = json!({
"Subnets": []
});
let ipam: Ipam = serde_json::from_value(raw).unwrap();
assert_eq!(ipam.ty, "Static");
}
#[test]
fn network_round_trip_nat() {
let net = HostComputeNetwork {
id: Some("netid".to_string()),
name: "zlayer-nat".to_string(),
ty: NetworkType::Nat,
ipams: vec![Ipam {
ty: "Static".to_string(),
subnets: vec![Subnet {
ip_address_prefix: "10.0.0.0/24".to_string(),
routes: vec![Route {
next_hop: "10.0.0.1".to_string(),
destination_prefix: "0.0.0.0/0".to_string(),
metric: Some(100),
}],
policies: vec![],
}],
}],
dns: Some(Dns {
server_list: vec!["1.1.1.1".to_string()],
..Dns::default()
}),
schema_version: SchemaVersion::default(),
..HostComputeNetwork::default()
};
let s = serde_json::to_string(&net).unwrap();
let back: HostComputeNetwork = serde_json::from_str(&s).unwrap();
assert_eq!(back.name, "zlayer-nat");
assert!(matches!(back.ty, NetworkType::Nat));
assert_eq!(back.ipams.len(), 1);
assert_eq!(back.ipams[0].subnets[0].ip_address_prefix, "10.0.0.0/24");
assert_eq!(back.ipams[0].subnets[0].routes[0].next_hop, "10.0.0.1");
assert_eq!(back.ipams[0].subnets[0].routes[0].metric, Some(100));
assert_eq!(
back.dns.as_ref().unwrap().server_list,
vec!["1.1.1.1".to_string()]
);
assert_eq!(back.schema_version, SchemaVersion::default());
}
#[test]
fn endpoint_stats_all_u64_fields_roundtrip() {
let raw = json!({
"EndpointId": "ep-guid",
"InstanceId": "inst-guid",
"BytesReceived": 10_000_000_001u64,
"BytesSent": 20_000_000_002u64,
"PacketsReceived": 300u64,
"PacketsSent": 400u64,
"DroppedPacketsIncoming": 5u64,
"DroppedPacketsOutgoing": 6u64
});
let stats: EndpointStats = serde_json::from_value(raw).unwrap();
assert_eq!(stats.endpoint_id, "ep-guid");
assert_eq!(stats.instance_id, "inst-guid");
assert_eq!(stats.bytes_received, 10_000_000_001);
assert_eq!(stats.bytes_sent, 20_000_000_002);
assert_eq!(stats.packets_received, 300);
assert_eq!(stats.packets_sent, 400);
assert_eq!(stats.dropped_packets_incoming, 5);
assert_eq!(stats.dropped_packets_outgoing, 6);
}
#[test]
fn modify_namespace_request_add_endpoint() {
let req = ModifyNamespaceSettingRequest {
resource_type: 1,
request_type: ModifyRequestType::Add,
settings: json!({ "EndpointId": "ep-guid" }),
};
let v: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(v["ResourceType"], json!(1));
assert_eq!(v["RequestType"], json!("Add"));
assert_eq!(v["Settings"]["EndpointId"], json!("ep-guid"));
}
use super::{
AclAction, AclDirection, AclPolicySetting, EndpointPolicy, EndpointPolicyType,
OutBoundNatPolicySetting, SdnRoutePolicySetting,
};
#[test]
fn out_bound_nat_wire_format_exactly_matches_hcsshim() {
let policy = EndpointPolicy::out_bound_nat(vec!["10.200.0.0/16".to_string()]);
let v: serde_json::Value = serde_json::to_value(&policy).unwrap();
let expected = json!({
"Type": "OutBoundNAT",
"Settings": { "Exceptions": ["10.200.0.0/16"] }
});
assert_eq!(v, expected);
}
#[test]
fn out_bound_nat_empty_exceptions_omits_field() {
let policy = EndpointPolicy {
ty: EndpointPolicyType::OutBoundNat,
settings: serde_json::to_value(OutBoundNatPolicySetting::default()).unwrap(),
};
let v: serde_json::Value = serde_json::to_value(&policy).unwrap();
assert_eq!(v, json!({"Type": "OutBoundNAT", "Settings": {}}));
}
#[test]
fn sdn_route_wire_format_no_encap() {
let policy = EndpointPolicy::sdn_route("10.200.0.0/16", false);
let v: serde_json::Value = serde_json::to_value(&policy).unwrap();
let expected = json!({
"Type": "SDNRoute",
"Settings": {
"DestinationPrefix": "10.200.0.0/16",
"NeedEncap": false,
}
});
assert_eq!(v, expected);
}
#[test]
fn sdn_route_wire_format_need_encap_true() {
let policy = EndpointPolicy::sdn_route("10.96.0.0/12", true);
let v: serde_json::Value = serde_json::to_value(&policy).unwrap();
let expected = json!({
"Type": "SDNRoute",
"Settings": {
"DestinationPrefix": "10.96.0.0/12",
"NeedEncap": true,
}
});
assert_eq!(v, expected);
}
#[test]
fn acl_in_allow_wire_format() {
let policy = EndpointPolicy::acl_in_allow("10.200.0.0/16");
let v: serde_json::Value = serde_json::to_value(&policy).unwrap();
let expected = json!({
"Type": "ACL",
"Settings": {
"Action": "Allow",
"Direction": "In",
"RemoteAddresses": "10.200.0.0/16",
}
});
assert_eq!(v, expected);
}
#[test]
fn acl_full_form_with_protocols_ports_priority() {
let settings = AclPolicySetting {
protocols: "6,17".to_string(),
action: AclAction::Block,
direction: AclDirection::Out,
local_addresses: "10.0.0.21".to_string(),
remote_addresses: "192.168.100.0/24".to_string(),
local_ports: "80,8080".to_string(),
remote_ports: "80,8080".to_string(),
rule_type: "Switch".to_string(),
priority: 200,
};
let v = serde_json::to_value(settings).unwrap();
assert_eq!(
v,
json!({
"Protocols": "6,17",
"Action": "Block",
"Direction": "Out",
"LocalAddresses": "10.0.0.21",
"RemoteAddresses": "192.168.100.0/24",
"LocalPorts": "80,8080",
"RemotePorts": "80,8080",
"RuleType": "Switch",
"Priority": 200,
})
);
}
#[test]
fn endpoint_policy_type_tag_spellings() {
let cases = [
(EndpointPolicyType::PortMapping, "PortMapping"),
(EndpointPolicyType::Acl, "ACL"),
(EndpointPolicyType::Qos, "QOS"),
(EndpointPolicyType::OutBoundNat, "OutBoundNAT"),
(EndpointPolicyType::SdnRoute, "SDNRoute"),
(EndpointPolicyType::L4Proxy, "L4Proxy"),
(EndpointPolicyType::L4WfpProxy, "L4WFPPROXY"),
(EndpointPolicyType::PortName, "PortName"),
(EndpointPolicyType::EncapOverhead, "EncapOverhead"),
(
EndpointPolicyType::InterfaceConstraint,
"InterfaceConstraint",
),
(EndpointPolicyType::ProviderAddress, "ProviderAddress"),
(EndpointPolicyType::Iov, "Iov"),
(EndpointPolicyType::TierAcl, "TierAcl"),
(EndpointPolicyType::NetworkAcl, "NetworkACL"),
(EndpointPolicyType::NetworkL4Proxy, "NetworkL4Proxy"),
];
for (variant, expected) in cases {
let v = serde_json::to_value(variant).unwrap();
assert_eq!(v, serde_json::Value::String(expected.to_string()));
let back: EndpointPolicyType = serde_json::from_value(v).unwrap();
assert_eq!(back, variant);
}
}
#[test]
fn endpoint_policy_into_value_pushes_into_policies_vec() {
let policies: Vec<serde_json::Value> = vec![
EndpointPolicy::out_bound_nat(vec!["10.200.0.0/16".to_string()]).into(),
EndpointPolicy::sdn_route("10.200.0.0/16", false).into(),
EndpointPolicy::acl_in_allow("10.200.0.0/16").into(),
];
let ep = HostComputeEndpoint {
name: "ep".to_string(),
host_compute_network: "net-guid".to_string(),
policies,
..HostComputeEndpoint::default()
};
let s = serde_json::to_string(&ep).unwrap();
assert!(s.contains("\"OutBoundNAT\""));
assert!(s.contains("\"SDNRoute\""));
assert!(s.contains("\"ACL\""));
}
#[test]
fn sdn_route_with_next_hop_round_trips() {
let settings = SdnRoutePolicySetting {
destination_prefix: "10.96.0.0/12".to_string(),
next_hop: "10.200.0.1".to_string(),
need_encap: true,
};
let v = serde_json::to_value(settings.clone()).unwrap();
let back: SdnRoutePolicySetting = serde_json::from_value(v).unwrap();
assert_eq!(settings, back);
}
}