1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::convert::TryFrom;
4
5use sysknife_proto::sysknife::v1 as proto;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct DistroHint {
27 pub family: &'static str,
32 pub version: Option<String>,
35}
36
37pub const DISTRO_FAMILY_FEDORA: &str = "fedora";
39
40pub const DISTRO_FAMILY_DEBIAN: &str = "debian";
42
43pub const DISTRO_FAMILY_OTHER: &str = "other";
45
46pub const KNOWN_ACTION_NAMES: &[&str] = &[
71 "GetSystemState",
72 "CollectDiagnostics",
73 "GetDeploymentHistory",
74 "ListDeployments",
75 "UpdateSystem",
76 "CleanupDeployments",
77 "RebootSystem",
78 "RollbackDeployment",
79 "GetKernelArguments",
80 "PinDeployment",
81 "UnpinDeployment",
82 "RebaseSystem",
83 "SetKernelArguments",
84 "InstallFlatpak",
85 "RemoveFlatpak",
86 "UpdateFlatpak",
87 "SearchFlatpakApps",
88 "ListFlatpakRemotes",
89 "ListInstalledFlatpaks",
90 "AddFlatpakRemote",
91 "RemoveFlatpakRemote",
92 "GetFlatpakAppInfo",
93 "ListToolboxes",
94 "CreateToolbox",
95 "RemoveToolbox",
96 "GetLayeredPackages",
97 "ResetLayeredPackageOverride",
98 "GetPendingUpdates",
99 "InstallPackages",
100 "RemovePackages",
101 "AddLayeredPackage",
102 "RemoveLayeredPackage",
103 "ReplaceLayeredPackage",
104 "RemoveBasePackage",
105 "ListServices",
106 "ListTimers",
107 "ReloadDaemon",
108 "CreateScheduledJob",
109 "StartService",
110 "StopService",
111 "RestartService",
112 "ReloadService",
113 "SetServiceEnabled",
114 "MaskService",
115 "UnmaskService",
116 "GetServiceLogs",
117 "GetServiceStatus",
118 "GetServiceResourceLimits",
119 "SetServiceResourceLimits",
120 "GetFirewallState",
121 "GetNetworkStatus",
122 "GetListeningPorts",
123 "ConfigureWifi",
124 "SetDnsServers",
125 "ConfigureFirewall",
126 "GetDiskUsage",
127 "ListProcesses",
128 "SignalProcess",
129 "GetMemoryInfo",
130 "GetJournalLog",
132 "VacuumJournal",
133 "GetLvmReport",
135 "ExtendLogicalVolume",
136 "CreateLogicalVolume",
137 "CreateLvSnapshot",
138 "GetSysctl",
140 "SetSysctl",
141 "GetMounts",
143 "AddMount",
144 "RemoveMount",
145 "AddSwap",
146 "RemoveSwap",
147 "GetSudoGrants",
149 "GrantSudoAccess",
150 "RevokeSudoAccess",
151 "GetLogrotateStatus",
153 "ConfigureLogRotation",
154 "RemoveLogRotation",
155 "ConfigureRemoteSyslog",
156 "RemoveRemoteSyslog",
157 "GetPasswordAging",
159 "SetPasswordAging",
160 "SetPasswordPolicy",
161 "SetAccountLockout",
162 "GetAuditRules",
164 "AddAuditRule",
165 "RemoveAuditRule",
166 "GetCertificates",
168 "ObtainCertificate",
169 "RenewCertificates",
170 "GetDateTime",
171 "SetHostname",
172 "SetTimezone",
173 "SetLocale",
174 "SetNtp",
175 "ListPackageRepositories",
176 "AddPackageRepository",
177 "RemovePackageRepository",
178 "EnablePackageRepository",
179 "DisablePackageRepository",
180 "ListContainers",
181 "CreateContainer",
182 "StartContainer",
183 "StopContainer",
184 "RemoveContainer",
185 "GetContainerInfo",
186 "ListUsers",
187 "ListGroups",
188 "CreateUser",
189 "DeleteUser",
190 "AddUserToGroup",
191 "RemoveUserFromGroup",
192 "CreateGroup",
193 "DeleteGroup",
194 "LockUserAccount",
195 "UnlockUserAccount",
196 "GetAuthorizedKeys",
197 "AddAuthorizedKey",
198 "RemoveAuthorizedKey",
199 "SetSshdOption",
200 "ListJobHistory",
201 "AptUpdate",
203 "AptUpgrade",
204 "AptInstall",
205 "AptRemove",
206 "AptPurge",
207 "AptAutoremove",
208 "AptHold",
209 "AptUnhold",
210 "AptSearch",
211 "AptListInstalled",
212 "AptShow",
213 "AptListUpgradable",
214 "AptHistoryList",
215 "ConfigureUnattendedUpgrades",
216 "GetAptPins",
217 "SetAptPin",
218 "RemoveAptPin",
219 "AddPpa",
221 "RemovePpa",
222 "SnapInstall",
224 "SnapRemove",
225 "SnapRefresh",
226 "SnapHold",
227 "SnapUnhold",
228 "SnapList",
229 "SnapInfo",
230 "SnapRevert",
231 "SnapClassicInstall",
232 "UfwEnable",
234 "UfwDisable",
235 "UfwAllow",
236 "UfwDeny",
237 "UfwReset",
238 "UfwStatus",
239 "DistroboxList",
241 "DistroboxCreate",
242 "DistroboxRemove",
243 "NetplanGetConfig",
245 "NetplanApply",
246 "NetplanSet",
247 "NetplanGenerate",
248 "GrubGetKargs",
250 "GrubSetKargs",
251 "CheckPendingReboot",
253 "ResolvectlStatus",
255 "ResolvectlSetDns",
256 "AppArmorStatus",
258 "AppArmorEnforce",
259 "AppArmorComplain",
260 "CloudInitStatus",
262 "UbuntuInstallFlatpak",
264 "UbuntuRemoveFlatpak",
265 "UbuntuUpdateFlatpak",
266 "UbuntuListFlatpaks",
267 "Fail2banStatus",
269 "Fail2banBanIp",
270 "Fail2banUnbanIp",
271 "ConfigureFail2banJail",
272 "UbuntuReleaseUpgrade",
274 "ProStatus",
276 "ProAttach",
277 "ProDetach",
278 "EnableProService",
279 "DisableProService",
280 "LivepatchStatus",
282 "MultipassList",
284 "UfwDeleteRule",
286 "UfwLimit",
287];
288
289#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
296#[serde(transparent)]
297pub struct ActionName(String);
298
299impl ActionName {
300 pub fn parse(name: impl Into<String>) -> Result<Self, UnknownActionName> {
302 let name = name.into();
303 if KNOWN_ACTION_NAMES.contains(&name.as_str()) {
304 Ok(Self(name))
305 } else {
306 Err(UnknownActionName(name))
307 }
308 }
309
310 pub fn as_str(&self) -> &str {
312 &self.0
313 }
314}
315
316impl std::fmt::Display for ActionName {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 f.write_str(&self.0)
319 }
320}
321
322impl AsRef<str> for ActionName {
323 fn as_ref(&self) -> &str {
324 &self.0
325 }
326}
327
328#[derive(Clone, Debug, PartialEq, Eq)]
330pub struct UnknownActionName(pub String);
331
332impl std::fmt::Display for UnknownActionName {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 write!(f, "unknown action name: '{}'", self.0)
335 }
336}
337
338impl std::error::Error for UnknownActionName {}
339
340#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
349#[serde(transparent)]
350pub struct RequestHash(String);
351
352impl RequestHash {
353 pub fn new(s: impl Into<String>) -> Self {
354 Self(s.into())
355 }
356 pub fn as_str(&self) -> &str {
357 &self.0
358 }
359 pub fn into_inner(self) -> String {
360 self.0
361 }
362}
363
364impl std::fmt::Display for RequestHash {
365 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366 f.write_str(&self.0)
367 }
368}
369
370impl AsRef<str> for RequestHash {
371 fn as_ref(&self) -> &str {
372 &self.0
373 }
374}
375
376impl From<String> for RequestHash {
377 fn from(s: String) -> Self {
378 Self(s)
379 }
380}
381
382#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
383#[serde(rename_all = "snake_case")]
384pub enum CallerRole {
385 Observer,
386 Dev,
387 Admin,
388 Boot,
389}
390
391#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
392#[serde(rename_all = "snake_case")]
393pub enum RiskLevel {
394 Low,
395 Medium,
396 High,
397}
398
399#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
400#[serde(rename_all = "snake_case")]
401pub enum JobState {
402 Queued,
403 Running,
404 Succeeded,
405 Failed,
406 Canceled,
407 RolledBack,
408 NeedsReboot,
409}
410
411#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
418#[serde(rename_all = "snake_case")]
419pub enum FailureCategory {
420 ValidationFailure,
421 AuthorizationFailure,
422 PolicyDenied,
423 StaleApproval,
424 ExecutionFailure,
425 TransientInfrastructureFailure,
426 Cancellation,
427 StuckExecution,
428 RebootRequired,
429 RollbackFailure,
430}
431
432#[derive(Debug, thiserror::Error)]
433pub enum BridgeError {
434 #[error("invalid json payload for {0}")]
435 InvalidJson(&'static str, #[source] serde_json::Error),
436
437 #[error("invalid enum value {value} for {field}")]
438 InvalidEnum { field: &'static str, value: i32 },
439}
440
441#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
442pub struct RequestEnvelope {
443 pub action_name: String,
444 pub request_id: String,
445 pub params: Value,
446 pub caller_role: CallerRole,
447 pub request_hash: RequestHash,
448}
449
450#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
451pub struct PreviewEnvelope {
452 pub summary: String,
453 pub risk_level: RiskLevel,
454 pub current_state: Value,
455 pub proposed_change: Value,
456 pub expected_side_effects: Vec<String>,
457 pub reboot_required: bool,
458 pub rollback_available: bool,
459 pub warnings: Vec<String>,
460 pub request_hash: RequestHash,
461}
462
463#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
464pub struct ResultEnvelope {
465 pub status: JobState,
466 pub summary: String,
467 pub warnings: Vec<String>,
468 pub job_id: Option<String>,
469 pub needs_reboot: bool,
470 pub rollback_ref: Option<String>,
471 pub transaction_id: String,
472}
473
474#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
475pub struct TransactionRecord {
476 pub transaction_id: String,
477 pub request_id: String,
478 pub request_hash: String,
479 pub action_name: String,
480 pub risk_level: RiskLevel,
481 pub status: JobState,
482 pub approval_id: Option<String>,
483 pub summary: String,
484 pub warnings: Vec<String>,
485}
486
487fn caller_role_code(value: CallerRole) -> i32 {
488 match value {
489 CallerRole::Observer => 1,
490 CallerRole::Dev => 2,
491 CallerRole::Admin => 3,
492 CallerRole::Boot => 4,
493 }
494}
495
496fn risk_level_code(value: RiskLevel) -> i32 {
497 match value {
498 RiskLevel::Low => 1,
499 RiskLevel::Medium => 2,
500 RiskLevel::High => 3,
501 }
502}
503
504fn job_state_code(value: JobState) -> i32 {
505 match value {
506 JobState::Queued => 1,
507 JobState::Running => 2,
508 JobState::Succeeded => 3,
509 JobState::Failed => 4,
510 JobState::Canceled => 5,
511 JobState::RolledBack => 6,
512 JobState::NeedsReboot => 7,
513 }
514}
515
516fn failure_category_code(value: FailureCategory) -> i32 {
517 match value {
518 FailureCategory::ValidationFailure => 1,
519 FailureCategory::AuthorizationFailure => 2,
520 FailureCategory::PolicyDenied => 3,
521 FailureCategory::StaleApproval => 4,
522 FailureCategory::ExecutionFailure => 5,
523 FailureCategory::TransientInfrastructureFailure => 6,
524 FailureCategory::Cancellation => 7,
525 FailureCategory::StuckExecution => 8,
526 FailureCategory::RebootRequired => 9,
527 FailureCategory::RollbackFailure => 10,
528 }
529}
530
531impl From<CallerRole> for proto::CallerRole {
532 fn from(value: CallerRole) -> Self {
533 proto::CallerRole::try_from(caller_role_code(value)).expect("valid caller role")
534 }
535}
536
537impl TryFrom<proto::CallerRole> for CallerRole {
538 type Error = BridgeError;
539
540 fn try_from(value: proto::CallerRole) -> Result<Self, Self::Error> {
541 match i32::from(value) {
542 1 => Ok(CallerRole::Observer),
543 2 => Ok(CallerRole::Dev),
544 3 => Ok(CallerRole::Admin),
545 4 => Ok(CallerRole::Boot),
546 code => Err(BridgeError::InvalidEnum {
547 field: "caller_role",
548 value: code,
549 }),
550 }
551 }
552}
553
554impl From<RiskLevel> for proto::RiskLevel {
555 fn from(value: RiskLevel) -> Self {
556 proto::RiskLevel::try_from(risk_level_code(value)).expect("valid risk level")
557 }
558}
559
560impl TryFrom<proto::RiskLevel> for RiskLevel {
561 type Error = BridgeError;
562
563 fn try_from(value: proto::RiskLevel) -> Result<Self, Self::Error> {
564 match i32::from(value) {
565 1 => Ok(RiskLevel::Low),
566 2 => Ok(RiskLevel::Medium),
567 3 => Ok(RiskLevel::High),
568 code => Err(BridgeError::InvalidEnum {
569 field: "risk_level",
570 value: code,
571 }),
572 }
573 }
574}
575
576impl From<JobState> for proto::JobState {
577 fn from(value: JobState) -> Self {
578 proto::JobState::try_from(job_state_code(value)).expect("valid job state")
579 }
580}
581
582impl TryFrom<proto::JobState> for JobState {
583 type Error = BridgeError;
584
585 fn try_from(value: proto::JobState) -> Result<Self, Self::Error> {
586 match i32::from(value) {
587 1 => Ok(JobState::Queued),
588 2 => Ok(JobState::Running),
589 3 => Ok(JobState::Succeeded),
590 4 => Ok(JobState::Failed),
591 5 => Ok(JobState::Canceled),
592 6 => Ok(JobState::RolledBack),
593 7 => Ok(JobState::NeedsReboot),
594 code => Err(BridgeError::InvalidEnum {
595 field: "job_state",
596 value: code,
597 }),
598 }
599 }
600}
601
602impl From<FailureCategory> for proto::FailureCategory {
603 fn from(value: FailureCategory) -> Self {
604 proto::FailureCategory::try_from(failure_category_code(value))
605 .expect("valid failure category")
606 }
607}
608
609impl TryFrom<proto::FailureCategory> for FailureCategory {
610 type Error = BridgeError;
611
612 fn try_from(value: proto::FailureCategory) -> Result<Self, Self::Error> {
613 match i32::from(value) {
614 1 => Ok(FailureCategory::ValidationFailure),
615 2 => Ok(FailureCategory::AuthorizationFailure),
616 3 => Ok(FailureCategory::PolicyDenied),
617 4 => Ok(FailureCategory::StaleApproval),
618 5 => Ok(FailureCategory::ExecutionFailure),
619 6 => Ok(FailureCategory::TransientInfrastructureFailure),
620 7 => Ok(FailureCategory::Cancellation),
621 8 => Ok(FailureCategory::StuckExecution),
622 9 => Ok(FailureCategory::RebootRequired),
623 10 => Ok(FailureCategory::RollbackFailure),
624 code => Err(BridgeError::InvalidEnum {
625 field: "failure_category",
626 value: code,
627 }),
628 }
629 }
630}
631
632impl From<RequestEnvelope> for proto::RequestEnvelope {
633 fn from(value: RequestEnvelope) -> Self {
634 Self {
635 action_name: value.action_name,
636 request_id: value.request_id,
637 params_json: serde_json::to_string(&value.params).expect("json serialization"),
638 caller_role: caller_role_code(value.caller_role),
639 request_hash: value.request_hash.into_inner(),
640 }
641 }
642}
643
644impl TryFrom<proto::RequestEnvelope> for RequestEnvelope {
645 type Error = BridgeError;
646
647 fn try_from(value: proto::RequestEnvelope) -> Result<Self, Self::Error> {
648 let params = serde_json::from_str(&value.params_json)
649 .map_err(|error| BridgeError::InvalidJson("params_json", error))?;
650
651 Ok(Self {
652 action_name: value.action_name,
653 request_id: value.request_id,
654 params,
655 caller_role: CallerRole::try_from(
656 proto::CallerRole::try_from(value.caller_role).map_err(|_| {
657 BridgeError::InvalidEnum {
658 field: "caller_role",
659 value: value.caller_role,
660 }
661 })?,
662 )?,
663 request_hash: RequestHash::from(value.request_hash),
664 })
665 }
666}
667
668impl From<PreviewEnvelope> for proto::PreviewEnvelope {
669 fn from(value: PreviewEnvelope) -> Self {
670 Self {
671 summary: value.summary,
672 risk_level: risk_level_code(value.risk_level),
673 current_state_json: serde_json::to_string(&value.current_state)
674 .expect("json serialization"),
675 proposed_change_json: serde_json::to_string(&value.proposed_change)
676 .expect("json serialization"),
677 expected_side_effects: value.expected_side_effects,
678 reboot_required: value.reboot_required,
679 rollback_available: value.rollback_available,
680 warnings: value.warnings,
681 request_hash: value.request_hash.into_inner(),
682 }
683 }
684}
685
686impl TryFrom<proto::PreviewEnvelope> for PreviewEnvelope {
687 type Error = BridgeError;
688
689 fn try_from(value: proto::PreviewEnvelope) -> Result<Self, Self::Error> {
690 let current_state = serde_json::from_str(&value.current_state_json)
691 .map_err(|error| BridgeError::InvalidJson("current_state_json", error))?;
692 let proposed_change = serde_json::from_str(&value.proposed_change_json)
693 .map_err(|error| BridgeError::InvalidJson("proposed_change_json", error))?;
694
695 Ok(Self {
696 summary: value.summary,
697 risk_level: RiskLevel::try_from(
698 proto::RiskLevel::try_from(value.risk_level).map_err(|_| {
699 BridgeError::InvalidEnum {
700 field: "risk_level",
701 value: value.risk_level,
702 }
703 })?,
704 )?,
705 current_state,
706 proposed_change,
707 expected_side_effects: value.expected_side_effects,
708 reboot_required: value.reboot_required,
709 rollback_available: value.rollback_available,
710 warnings: value.warnings,
711 request_hash: RequestHash::from(value.request_hash),
712 })
713 }
714}
715
716impl From<ResultEnvelope> for proto::ResultEnvelope {
717 fn from(value: ResultEnvelope) -> Self {
718 Self {
719 status: job_state_code(value.status),
720 summary: value.summary,
721 warnings: value.warnings,
722 job_id: value.job_id.unwrap_or_default(),
723 needs_reboot: value.needs_reboot,
724 rollback_ref: value.rollback_ref.unwrap_or_default(),
725 transaction_id: value.transaction_id,
726 }
727 }
728}
729
730impl TryFrom<proto::ResultEnvelope> for ResultEnvelope {
731 type Error = BridgeError;
732
733 fn try_from(value: proto::ResultEnvelope) -> Result<Self, Self::Error> {
734 Ok(Self {
735 status: JobState::try_from(proto::JobState::try_from(value.status).map_err(|_| {
736 BridgeError::InvalidEnum {
737 field: "job_state",
738 value: value.status,
739 }
740 })?)?,
741 summary: value.summary,
742 warnings: value.warnings,
743 job_id: if value.job_id.is_empty() {
744 None
745 } else {
746 Some(value.job_id)
747 },
748 needs_reboot: value.needs_reboot,
749 rollback_ref: if value.rollback_ref.is_empty() {
750 None
751 } else {
752 Some(value.rollback_ref)
753 },
754 transaction_id: value.transaction_id,
755 })
756 }
757}
758
759impl From<TransactionRecord> for proto::TransactionRecord {
760 fn from(value: TransactionRecord) -> Self {
761 Self {
762 transaction_id: value.transaction_id,
763 request_id: value.request_id,
764 request_hash: value.request_hash,
765 action_name: value.action_name,
766 risk_level: risk_level_code(value.risk_level),
767 status: job_state_code(value.status),
768 approval_id: value.approval_id.unwrap_or_default(),
769 summary: value.summary,
770 warnings: value.warnings,
771 }
772 }
773}
774
775impl TryFrom<proto::TransactionRecord> for TransactionRecord {
776 type Error = BridgeError;
777
778 fn try_from(value: proto::TransactionRecord) -> Result<Self, Self::Error> {
779 Ok(Self {
780 transaction_id: value.transaction_id,
781 request_id: value.request_id,
782 request_hash: value.request_hash,
783 action_name: value.action_name,
784 risk_level: RiskLevel::try_from(
785 proto::RiskLevel::try_from(value.risk_level).map_err(|_| {
786 BridgeError::InvalidEnum {
787 field: "risk_level",
788 value: value.risk_level,
789 }
790 })?,
791 )?,
792 status: JobState::try_from(proto::JobState::try_from(value.status).map_err(|_| {
793 BridgeError::InvalidEnum {
794 field: "job_state",
795 value: value.status,
796 }
797 })?)?,
798 approval_id: if value.approval_id.is_empty() {
799 None
800 } else {
801 Some(value.approval_id)
802 },
803 summary: value.summary,
804 warnings: value.warnings,
805 })
806 }
807}
808
809#[cfg(test)]
810mod request_hash_tests {
811 use super::*;
812
813 #[test]
814 fn serde_round_trips_via_transparent_string() {
815 let r = RequestHash::new("abc123".to_string());
818 let json = serde_json::to_string(&r).unwrap();
819 assert_eq!(json, "\"abc123\"");
820 let back: RequestHash = serde_json::from_str("\"abc123\"").unwrap();
821 assert_eq!(back, r);
822 }
823}