use crate::api::generated::machine::{
EtcdAlarm as ProtoEtcdAlarm, EtcdAlarmDisarm as ProtoEtcdAlarmDisarm,
EtcdAlarmDisarmResponse as ProtoEtcdAlarmDisarmResponse,
EtcdAlarmListResponse as ProtoEtcdAlarmListResponse, EtcdDefragment as ProtoEtcdDefragment,
EtcdDefragmentResponse as ProtoEtcdDefragmentResponse,
EtcdForfeitLeadership as ProtoEtcdForfeitLeadership,
EtcdForfeitLeadershipRequest as ProtoEtcdForfeitLeadershipRequest,
EtcdForfeitLeadershipResponse as ProtoEtcdForfeitLeadershipResponse,
EtcdLeaveCluster as ProtoEtcdLeaveCluster,
EtcdLeaveClusterRequest as ProtoEtcdLeaveClusterRequest,
EtcdLeaveClusterResponse as ProtoEtcdLeaveClusterResponse, EtcdMember as ProtoEtcdMember,
EtcdMemberAlarm as ProtoEtcdMemberAlarm, EtcdMemberListRequest as ProtoEtcdMemberListRequest,
EtcdMemberListResponse as ProtoEtcdMemberListResponse,
EtcdMemberStatus as ProtoEtcdMemberStatus, EtcdMembers as ProtoEtcdMembers,
EtcdRemoveMemberByIdRequest as ProtoEtcdRemoveMemberByIdRequest,
EtcdRemoveMemberByIdResponse as ProtoEtcdRemoveMemberByIdResponse,
EtcdStatus as ProtoEtcdStatus, EtcdStatusResponse as ProtoEtcdStatusResponse,
};
#[derive(Debug, Clone, Default)]
pub struct EtcdMemberListRequest {
pub query_local: bool,
}
impl EtcdMemberListRequest {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn local() -> Self {
Self { query_local: true }
}
}
impl From<EtcdMemberListRequest> for ProtoEtcdMemberListRequest {
fn from(req: EtcdMemberListRequest) -> Self {
Self {
query_local: req.query_local,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EtcdMember {
pub id: u64,
pub hostname: String,
pub peer_urls: Vec<String>,
pub client_urls: Vec<String>,
pub is_learner: bool,
}
impl From<ProtoEtcdMember> for EtcdMember {
fn from(proto: ProtoEtcdMember) -> Self {
Self {
id: proto.id,
hostname: proto.hostname,
peer_urls: proto.peer_urls,
client_urls: proto.client_urls,
is_learner: proto.is_learner,
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdMembersResult {
pub node: Option<String>,
pub members: Vec<EtcdMember>,
}
impl From<ProtoEtcdMembers> for EtcdMembersResult {
fn from(proto: ProtoEtcdMembers) -> Self {
Self {
node: proto.metadata.map(|m| m.hostname),
members: proto.members.into_iter().map(EtcdMember::from).collect(),
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdMemberListResponse {
pub results: Vec<EtcdMembersResult>,
}
impl From<ProtoEtcdMemberListResponse> for EtcdMemberListResponse {
fn from(proto: ProtoEtcdMemberListResponse) -> Self {
Self {
results: proto
.messages
.into_iter()
.map(EtcdMembersResult::from)
.collect(),
}
}
}
impl EtcdMemberListResponse {
#[must_use]
pub fn all_members(&self) -> Vec<&EtcdMember> {
let mut seen_ids = std::collections::HashSet::new();
let mut members = Vec::new();
for result in &self.results {
for member in &result.members {
if seen_ids.insert(member.id) {
members.push(member);
}
}
}
members
}
#[must_use]
pub fn find_by_hostname(&self, hostname: &str) -> Option<&EtcdMember> {
self.all_members()
.into_iter()
.find(|m| m.hostname == hostname)
}
#[must_use]
pub fn find_by_id(&self, id: u64) -> Option<&EtcdMember> {
self.all_members().into_iter().find(|m| m.id == id)
}
}
#[derive(Debug, Clone)]
pub struct EtcdRemoveMemberByIdRequest {
pub member_id: u64,
}
impl EtcdRemoveMemberByIdRequest {
#[must_use]
pub fn new(member_id: u64) -> Self {
Self { member_id }
}
}
impl From<EtcdRemoveMemberByIdRequest> for ProtoEtcdRemoveMemberByIdRequest {
fn from(req: EtcdRemoveMemberByIdRequest) -> Self {
Self {
member_id: req.member_id,
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdRemoveMemberByIdResult {
pub node: Option<String>,
}
#[derive(Debug, Clone)]
pub struct EtcdRemoveMemberByIdResponse {
pub results: Vec<EtcdRemoveMemberByIdResult>,
}
impl From<ProtoEtcdRemoveMemberByIdResponse> for EtcdRemoveMemberByIdResponse {
fn from(proto: ProtoEtcdRemoveMemberByIdResponse) -> Self {
Self {
results: proto
.messages
.into_iter()
.map(|m| EtcdRemoveMemberByIdResult {
node: m.metadata.map(|meta| meta.hostname),
})
.collect(),
}
}
}
impl EtcdRemoveMemberByIdResponse {
#[must_use]
pub fn is_success(&self) -> bool {
!self.results.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct EtcdLeaveClusterRequest;
impl EtcdLeaveClusterRequest {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl From<EtcdLeaveClusterRequest> for ProtoEtcdLeaveClusterRequest {
fn from(_req: EtcdLeaveClusterRequest) -> Self {
Self {}
}
}
#[derive(Debug, Clone)]
pub struct EtcdLeaveClusterResult {
pub node: Option<String>,
}
impl From<ProtoEtcdLeaveCluster> for EtcdLeaveClusterResult {
fn from(proto: ProtoEtcdLeaveCluster) -> Self {
Self {
node: proto.metadata.map(|m| m.hostname),
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdLeaveClusterResponse {
pub results: Vec<EtcdLeaveClusterResult>,
}
impl From<ProtoEtcdLeaveClusterResponse> for EtcdLeaveClusterResponse {
fn from(proto: ProtoEtcdLeaveClusterResponse) -> Self {
Self {
results: proto
.messages
.into_iter()
.map(EtcdLeaveClusterResult::from)
.collect(),
}
}
}
impl EtcdLeaveClusterResponse {
#[must_use]
pub fn is_success(&self) -> bool {
!self.results.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct EtcdForfeitLeadershipRequest;
impl EtcdForfeitLeadershipRequest {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl From<EtcdForfeitLeadershipRequest> for ProtoEtcdForfeitLeadershipRequest {
fn from(_req: EtcdForfeitLeadershipRequest) -> Self {
Self {}
}
}
#[derive(Debug, Clone)]
pub struct EtcdForfeitLeadershipResult {
pub node: Option<String>,
pub member: String,
}
impl From<ProtoEtcdForfeitLeadership> for EtcdForfeitLeadershipResult {
fn from(proto: ProtoEtcdForfeitLeadership) -> Self {
Self {
node: proto.metadata.map(|m| m.hostname),
member: proto.member,
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdForfeitLeadershipResponse {
pub results: Vec<EtcdForfeitLeadershipResult>,
}
impl From<ProtoEtcdForfeitLeadershipResponse> for EtcdForfeitLeadershipResponse {
fn from(proto: ProtoEtcdForfeitLeadershipResponse) -> Self {
Self {
results: proto
.messages
.into_iter()
.map(EtcdForfeitLeadershipResult::from)
.collect(),
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdMemberStatus {
pub member_id: u64,
pub protocol_version: String,
pub storage_version: String,
pub db_size: i64,
pub db_size_in_use: i64,
pub leader: u64,
pub raft_index: u64,
pub raft_term: u64,
pub raft_applied_index: u64,
pub errors: Vec<String>,
pub is_learner: bool,
}
impl From<ProtoEtcdMemberStatus> for EtcdMemberStatus {
fn from(proto: ProtoEtcdMemberStatus) -> Self {
Self {
member_id: proto.member_id,
protocol_version: proto.protocol_version,
storage_version: proto.storage_version,
db_size: proto.db_size,
db_size_in_use: proto.db_size_in_use,
leader: proto.leader,
raft_index: proto.raft_index,
raft_term: proto.raft_term,
raft_applied_index: proto.raft_applied_index,
errors: proto.errors,
is_learner: proto.is_learner,
}
}
}
impl EtcdMemberStatus {
#[must_use]
pub fn is_leader(&self) -> bool {
self.member_id == self.leader
}
#[must_use]
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
#[must_use]
pub fn db_size_human(&self) -> String {
humanize_bytes(self.db_size as u64)
}
}
#[derive(Debug, Clone)]
pub struct EtcdStatusResult {
pub node: Option<String>,
pub member_status: Option<EtcdMemberStatus>,
}
impl From<ProtoEtcdStatus> for EtcdStatusResult {
fn from(proto: ProtoEtcdStatus) -> Self {
Self {
node: proto.metadata.map(|m| m.hostname),
member_status: proto.member_status.map(EtcdMemberStatus::from),
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdStatusResponse {
pub results: Vec<EtcdStatusResult>,
}
impl From<ProtoEtcdStatusResponse> for EtcdStatusResponse {
fn from(proto: ProtoEtcdStatusResponse) -> Self {
Self {
results: proto
.messages
.into_iter()
.map(EtcdStatusResult::from)
.collect(),
}
}
}
impl EtcdStatusResponse {
#[must_use]
pub fn first(&self) -> Option<&EtcdMemberStatus> {
self.results.first().and_then(|r| r.member_status.as_ref())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EtcdAlarmType {
None,
NoSpace,
Corrupt,
}
impl From<i32> for EtcdAlarmType {
fn from(value: i32) -> Self {
match value {
1 => Self::NoSpace,
2 => Self::Corrupt,
_ => Self::None,
}
}
}
impl std::fmt::Display for EtcdAlarmType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EtcdAlarmType::None => write!(f, "none"),
EtcdAlarmType::NoSpace => write!(f, "NOSPACE"),
EtcdAlarmType::Corrupt => write!(f, "CORRUPT"),
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdMemberAlarm {
pub member_id: u64,
pub alarm: EtcdAlarmType,
}
impl From<ProtoEtcdMemberAlarm> for EtcdMemberAlarm {
fn from(proto: ProtoEtcdMemberAlarm) -> Self {
Self {
member_id: proto.member_id,
alarm: EtcdAlarmType::from(proto.alarm),
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdAlarmResult {
pub node: Option<String>,
pub member_alarms: Vec<EtcdMemberAlarm>,
}
impl From<ProtoEtcdAlarm> for EtcdAlarmResult {
fn from(proto: ProtoEtcdAlarm) -> Self {
Self {
node: proto.metadata.map(|m| m.hostname),
member_alarms: proto
.member_alarms
.into_iter()
.map(EtcdMemberAlarm::from)
.collect(),
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdAlarmListResponse {
pub results: Vec<EtcdAlarmResult>,
}
impl From<ProtoEtcdAlarmListResponse> for EtcdAlarmListResponse {
fn from(proto: ProtoEtcdAlarmListResponse) -> Self {
Self {
results: proto
.messages
.into_iter()
.map(EtcdAlarmResult::from)
.collect(),
}
}
}
impl EtcdAlarmListResponse {
#[must_use]
pub fn has_alarms(&self) -> bool {
self.results.iter().any(|r| {
r.member_alarms
.iter()
.any(|a| a.alarm != EtcdAlarmType::None)
})
}
#[must_use]
pub fn active_alarms(&self) -> Vec<&EtcdMemberAlarm> {
self.results
.iter()
.flat_map(|r| r.member_alarms.iter())
.filter(|a| a.alarm != EtcdAlarmType::None)
.collect()
}
}
#[derive(Debug, Clone)]
pub struct EtcdAlarmDisarmResult {
pub node: Option<String>,
pub member_alarms: Vec<EtcdMemberAlarm>,
}
impl From<ProtoEtcdAlarmDisarm> for EtcdAlarmDisarmResult {
fn from(proto: ProtoEtcdAlarmDisarm) -> Self {
Self {
node: proto.metadata.map(|m| m.hostname),
member_alarms: proto
.member_alarms
.into_iter()
.map(EtcdMemberAlarm::from)
.collect(),
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdAlarmDisarmResponse {
pub results: Vec<EtcdAlarmDisarmResult>,
}
impl From<ProtoEtcdAlarmDisarmResponse> for EtcdAlarmDisarmResponse {
fn from(proto: ProtoEtcdAlarmDisarmResponse) -> Self {
Self {
results: proto
.messages
.into_iter()
.map(EtcdAlarmDisarmResult::from)
.collect(),
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdDefragmentResult {
pub node: Option<String>,
}
impl From<ProtoEtcdDefragment> for EtcdDefragmentResult {
fn from(proto: ProtoEtcdDefragment) -> Self {
Self {
node: proto.metadata.map(|m| m.hostname),
}
}
}
#[derive(Debug, Clone)]
pub struct EtcdDefragmentResponse {
pub results: Vec<EtcdDefragmentResult>,
}
impl From<ProtoEtcdDefragmentResponse> for EtcdDefragmentResponse {
fn from(proto: ProtoEtcdDefragmentResponse) -> Self {
Self {
results: proto
.messages
.into_iter()
.map(EtcdDefragmentResult::from)
.collect(),
}
}
}
impl EtcdDefragmentResponse {
#[must_use]
pub fn is_success(&self) -> bool {
!self.results.is_empty()
}
}
fn humanize_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{bytes} B")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_etcd_member_list_request() {
let req = EtcdMemberListRequest::new();
assert!(!req.query_local);
let req = EtcdMemberListRequest::local();
assert!(req.query_local);
}
#[test]
fn test_etcd_remove_member_by_id_request() {
let req = EtcdRemoveMemberByIdRequest::new(12345);
assert_eq!(req.member_id, 12345);
let proto: ProtoEtcdRemoveMemberByIdRequest = req.into();
assert_eq!(proto.member_id, 12345);
}
#[test]
fn test_etcd_alarm_type() {
assert_eq!(EtcdAlarmType::from(0), EtcdAlarmType::None);
assert_eq!(EtcdAlarmType::from(1), EtcdAlarmType::NoSpace);
assert_eq!(EtcdAlarmType::from(2), EtcdAlarmType::Corrupt);
assert_eq!(EtcdAlarmType::from(99), EtcdAlarmType::None);
assert_eq!(EtcdAlarmType::NoSpace.to_string(), "NOSPACE");
assert_eq!(EtcdAlarmType::Corrupt.to_string(), "CORRUPT");
}
#[test]
fn test_etcd_member_status_is_leader() {
let status = EtcdMemberStatus {
member_id: 100,
protocol_version: "3.5.0".to_string(),
storage_version: "3.5".to_string(),
db_size: 10 * 1024 * 1024,
db_size_in_use: 5 * 1024 * 1024,
leader: 100,
raft_index: 1000,
raft_term: 5,
raft_applied_index: 999,
errors: vec![],
is_learner: false,
};
assert!(status.is_leader());
assert!(!status.has_errors());
assert_eq!(status.db_size_human(), "10.00 MB");
}
#[test]
fn test_humanize_bytes() {
assert_eq!(humanize_bytes(500), "500 B");
assert_eq!(humanize_bytes(1024), "1.00 KB");
assert_eq!(humanize_bytes(1536), "1.50 KB");
assert_eq!(humanize_bytes(1024 * 1024), "1.00 MB");
assert_eq!(humanize_bytes(1024 * 1024 * 1024), "1.00 GB");
}
#[test]
fn test_etcd_leave_cluster_request() {
let req = EtcdLeaveClusterRequest::new();
let _proto: ProtoEtcdLeaveClusterRequest = req.into();
}
#[test]
fn test_etcd_forfeit_leadership_request() {
let req = EtcdForfeitLeadershipRequest::new();
let _proto: ProtoEtcdForfeitLeadershipRequest = req.into();
}
}