Skip to main content

regent_sdk/state/attribute/system/
service.rs

1//! Service management attribute
2//!
3//! This module provides the `ServiceExpectedState` type for managing system services.
4//!
5//! **Compatible OS:**
6//! - Linux (all distributions with systemd) - uses `systemctl`
7//! - Windows (when `windows` feature is enabled) - uses `sc.exe` and `net` commands
8//!
9//! # Examples
10//!
11//! ## Rust API
12//!
13//! ```no_run
14//! use regent_sdk::state::attribute::system::service::ServiceExpectedState;
15//! use regent_sdk::{Attribute, ExpectedState, Privilege};
16//!
17//! // Ensure httpd service is running and enabled
18//! let httpd = ServiceExpectedState::started_and_enabled("httpd");
19//!
20//! // Ensure nginx is running but not started at boot
21//! let nginx = ServiceExpectedState::started_and_disabled("nginx");
22//!
23//! // Ensure cups is stopped and not started at boot
24//! let cups = ServiceExpectedState::stopped_and_disabled("cups");
25//!
26//! // Just manage whether service is enabled at boot
27//! let mysql = ServiceExpectedState::enabled("mysql", true);
28//! let postfix = ServiceExpectedState::disabled("postfix");
29//!
30//! // Manage unconditional action (restart)
31//! let redis = ServiceExpectedState::restarted("redis");
32//!
33//! let expected_state = ExpectedState::new()
34//!     .with_attribute(Attribute::service(httpd, Privilege::WithSudo, None))
35//!     .build();
36//! ```
37//!
38//! ## YAML API
39//!
40//! ```yaml
41//! Attributes:
42//!   - Name: Httpd must be running and enabled
43//!     Privilege: !WithSudo
44//!     Detail: !Service
45//!       Name: httpd
46//!       State: Started
47//!       Enabled: true
48//! ```
49//!
50//! For state-only configuration:
51//!
52//! ```yaml
53//! Attributes:
54//!   - Name: Nginx must be stopped
55//!     Privilege: !WithSudo
56//!     Detail: !Service
57//!       Name: nginx
58//!       State: Stopped
59//! ```
60//!
61//! For enabled-only configuration:
62//!
63//! ```yaml
64//! Attributes:
65//!   - Name: MySQL must be disabled at boot
66//!     Privilege: !WithSudo
67//!     Detail: !Service
68//!       Name: mysql
69//!       Enabled: false
70//! ```
71
72use crate::error::RegentError;
73use crate::hosts::managed_host::InternalApiCallOutcome;
74use crate::hosts::managed_host::{AssessCompliance, ReachCompliance, Timeout};
75use crate::hosts::properties::{HostProperties, InitSystem, LinuxFlavor, LinuxSpecifics, OsKind};
76use crate::secrets::SecretProvidersPool;
77use crate::state::Check;
78use crate::state::attribute::HostHandler;
79use crate::state::attribute::Privilege;
80use crate::state::attribute::Remediation;
81use crate::state::attribute::RemediationsList;
82use crate::state::compliance::AttributeComplianceAssessment;
83use serde::{Deserialize, Serialize};
84use std::time::Duration;
85
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87#[serde(rename_all = "PascalCase")]
88pub enum ServiceExpectedStatus {
89    Started,
90    Stopped,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94#[serde(rename_all = "PascalCase")]
95pub enum ServiceAction {
96    Restarted,
97    Reloaded,
98}
99
100/// Configuration for a system service
101///
102/// This enum represents the desired state for a system service, supporting configurations:
103/// - `State`: Manage the service's running state (started/stopped) and/or boot enablement
104/// - `Action`: Manage unconditional actions (restart/reload)
105///
106/// # YAML Representation
107///
108/// ## State with enabled:
109/// ```yaml
110/// Name: httpd
111/// State: Started
112/// Enabled: true
113/// ```
114///
115/// ## State only:
116/// ```yaml
117/// Name: nginx
118/// State: Started
119/// ```
120///
121/// ## Enabled only:
122/// ```yaml
123/// Name: mysql
124/// Enabled: true
125/// ```
126///
127/// ## Action only:
128/// ```yaml
129/// Name: nginx
130/// Action: Restarted
131/// ```
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
133#[serde(rename_all_fields = "PascalCase")]
134#[serde(untagged)]
135pub enum ServiceExpectedState {
136    /// Manage the service's running state and/or boot enablement
137    ///
138    /// Note: The `enabled` field is only present in this variant to respect the semantics
139    /// that enabled state is managed alongside running state.
140    State {
141        /// Name of the service
142        name: String,
143        /// Desired running state of the service (optional - can be omitted for enabled-only config)
144        #[serde(default)]
145        state: Option<ServiceExpectedStatus>,
146        /// Whether the service should be enabled at boot
147        enabled: bool,
148    },
149    /// Manage unconditional actions (restart/reload)
150    Action {
151        /// Name of the service
152        name: String,
153        /// Action to perform on the service
154        action: ServiceAction,
155    },
156}
157
158impl Timeout for ServiceExpectedState {
159    fn default_timeout(&self) -> Duration {
160        Duration::from_secs(10)
161    }
162}
163
164impl ServiceExpectedState {
165    /// Create a state configuration with running state and boot enablement
166    pub fn state(name: &str, state: ServiceExpectedStatus, enabled: bool) -> ServiceExpectedState {
167        ServiceExpectedState::State {
168            name: name.to_string(),
169            state: Some(state),
170            enabled,
171        }
172    }
173
174    /// Create a state configuration: running and enabled at boot
175    pub fn started_and_enabled(name: &str) -> ServiceExpectedState {
176        ServiceExpectedState::State {
177            name: name.to_string(),
178            state: Some(ServiceExpectedStatus::Started),
179            enabled: true,
180        }
181    }
182
183    /// Create a state configuration: running but disabled at boot
184    pub fn started_and_disabled(name: &str) -> ServiceExpectedState {
185        Self::state(name, ServiceExpectedStatus::Started, false)
186    }
187
188    /// Create a state configuration: stopped but enabled at boot
189    pub fn stopped_and_enabled(name: &str) -> ServiceExpectedState {
190        Self::state(name, ServiceExpectedStatus::Stopped, true)
191    }
192
193    /// Create a state configuration: stopped and disabled at boot
194    pub fn stopped_and_disabled(name: &str) -> ServiceExpectedState {
195        Self::state(name, ServiceExpectedStatus::Stopped, false)
196    }
197
198    /// Create an enabled-only configuration (no state management, only enablement)
199    pub fn enabled(name: &str, enabled: bool) -> ServiceExpectedState {
200        ServiceExpectedState::State {
201            name: name.to_string(),
202            state: None,
203            enabled,
204        }
205    }
206
207    /// Create a disabled-only configuration (no state management, only enablement)
208    pub fn disabled(name: &str) -> ServiceExpectedState {
209        Self::enabled(name, false)
210    }
211
212    /// Create a restarted action configuration
213    pub fn restarted(name: &str) -> ServiceExpectedState {
214        ServiceExpectedState::Action {
215            name: name.to_string(),
216            action: ServiceAction::Restarted,
217        }
218    }
219
220    /// Create a reloaded action configuration
221    pub fn reloaded(name: &str) -> ServiceExpectedState {
222        ServiceExpectedState::Action {
223            name: name.to_string(),
224            action: ServiceAction::Reloaded,
225        }
226    }
227}
228
229impl Check for ServiceExpectedState {
230    fn check(&self) -> Result<(), RegentError> {
231        Ok(())
232    }
233
234    fn check_host_compatibility(
235        &self,
236        host_properties: &HostProperties,
237    ) -> Result<(), RegentError> {
238        use crate::hosts::properties::InitSystem;
239        match host_properties.os_kind() {
240            OsKind::Linux(linux_specifics) => match linux_specifics.init_system {
241                InitSystem::Systemd => Ok(()),
242                InitSystem::Unknown => Err(RegentError::IncompatibleHost(
243                    "systemctl requires systemd but init system could not be detected".to_string(),
244                )),
245            },
246            #[cfg(feature = "windows")]
247            OsKind::Windows(_) => Ok(()),
248            incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
249                "Host is {:?} but service management is only supported on Linux with systemd or Windows (with windows feature)",
250                incompatible_os_kind
251            ))),
252        }
253    }
254}
255
256impl<Handler: HostHandler> AssessCompliance<Handler> for ServiceExpectedState {
257    async fn assess_compliance(
258        &self,
259        host_handler: &mut Handler,
260        host_properties: &Option<HostProperties>,
261        privilege: &Privilege,
262        _optional_secret_provider: &Option<SecretProvidersPool>,
263    ) -> Result<AttributeComplianceAssessment, RegentError> {
264        if let Some(props) = host_properties {
265            self.check_host_compatibility(props)?;
266        }
267
268        // Determine the effective OS kind - assume Linux if HostProperties is None
269        let os_kind = host_properties
270            .as_ref()
271            .map(|props| props.os_kind())
272            .unwrap_or(&OsKind::Linux(LinuxSpecifics {
273                linux_flavor: LinuxFlavor::Debian,
274                init_system: InitSystem::Systemd,
275            }));
276
277        match os_kind {
278            #[cfg(feature = "windows")]
279            OsKind::Windows(_) => {
280                let command_available = host_handler
281                    .is_this_command_available("sc", privilege)
282                    .await
283                    .unwrap_or(false);
284
285                if !command_available {
286                    return Err(RegentError::FailedDryRunEvaluation(
287                        "Service management commands (sc) are not available on this Windows host"
288                            .to_string(),
289                    ));
290                }
291            }
292            OsKind::Linux(_) => {
293                let command_available = host_handler
294                    .is_this_command_available("systemctl", privilege)
295                    .await
296                    .unwrap_or(false);
297
298                if !command_available {
299                    return Err(RegentError::FailedDryRunEvaluation(
300                        "Service management commands (systemctl) are not available on this Linux host".to_string(),
301                    ));
302                }
303            }
304            OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {}
305        }
306
307        let mut remediations: Vec<Remediation> = Vec::new();
308
309        match &self {
310            Self::State {
311                name,
312                state,
313                enabled,
314            } => {
315                // TODO : factorize this
316                match os_kind {
317                    #[cfg(feature = "windows")]
318                    OsKind::Windows(_) => {
319                        if let Some(expected_state) = state {
320                            match windows_service_is_active(host_handler, name).await {
321                                Ok(current_service_activity) => {
322                                    match (current_service_activity, expected_state) {
323                                        (
324                                            ServiceActivityStatus::Active,
325                                            ServiceExpectedStatus::Started,
326                                        )
327                                        | (
328                                            ServiceActivityStatus::Inactive,
329                                            ServiceExpectedStatus::Stopped,
330                                        ) => {}
331                                        (
332                                            ServiceActivityStatus::Active,
333                                            ServiceExpectedStatus::Stopped,
334                                        ) => {
335                                            remediations.push(Remediation::Service(
336                                                ServiceApiCall::from(
337                                                    ServiceModuleInternalApiCall::Stop(
338                                                        name.clone(),
339                                                    ),
340                                                    privilege.clone(),
341                                                ),
342                                            ));
343                                        }
344                                        (
345                                            ServiceActivityStatus::Inactive,
346                                            ServiceExpectedStatus::Started,
347                                        ) => {
348                                            remediations.push(Remediation::Service(
349                                                ServiceApiCall::from(
350                                                    ServiceModuleInternalApiCall::Start(
351                                                        name.clone(),
352                                                    ),
353                                                    privilege.clone(),
354                                                ),
355                                            ));
356                                        }
357                                        (
358                                            ServiceActivityStatus::ServiceNotFound,
359                                            _expected_state,
360                                        ) => {
361                                            return Ok(
362                                                AttributeComplianceAssessment::NonCompliantFatal(
363                                                    "service not found".to_string(),
364                                                ),
365                                            );
366                                        }
367                                        (
368                                            ServiceActivityStatus::UnsupportedCase(details),
369                                            _expected_state,
370                                        ) => {
371                                            return Ok(
372                                                AttributeComplianceAssessment::NonCompliantFatal(
373                                                    details.to_string(),
374                                                ),
375                                            );
376                                        }
377                                    }
378                                }
379                                Err(error_details) => {
380                                    return Err(RegentError::FailedTaskDryRun(format!(
381                                        "Unable to check service activity : {error_details}"
382                                    )));
383                                }
384                            }
385                        }
386
387                        match windows_service_is_enabled(host_handler, name).await {
388                            Ok(current_service_enabling) => {
389                                match (current_service_enabling, *enabled) {
390                                    (ServiceEnablingStatus::Enabled, true)
391                                    | (ServiceEnablingStatus::Disabled, false) => {}
392                                    (ServiceEnablingStatus::Enabled, false) => {
393                                        remediations.push(Remediation::Service(
394                                            ServiceApiCall::from(
395                                                ServiceModuleInternalApiCall::Disable(name.clone()),
396                                                privilege.clone(),
397                                            ),
398                                        ));
399                                        remediations.push(Remediation::Service(
400                                            ServiceApiCall::from(
401                                                ServiceModuleInternalApiCall::Enable(name.clone()),
402                                                privilege.clone(),
403                                            ),
404                                        ));
405                                    }
406                                    (ServiceEnablingStatus::Disabled, true) => {
407                                        remediations.push(Remediation::Service(
408                                            ServiceApiCall::from(
409                                                ServiceModuleInternalApiCall::Enable(name.clone()),
410                                                privilege.clone(),
411                                            ),
412                                        ));
413                                    }
414                                    (ServiceEnablingStatus::ServiceNotFound, _expected_state) => {
415                                        return Ok(
416                                            AttributeComplianceAssessment::NonCompliantFatal(
417                                                "service not found".to_string(),
418                                            ),
419                                        );
420                                    }
421                                    (
422                                        ServiceEnablingStatus::UnsupportedCase(details),
423                                        _expected_state,
424                                    ) => {
425                                        return Ok(
426                                            AttributeComplianceAssessment::NonCompliantFatal(
427                                                details.to_string(),
428                                            ),
429                                        );
430                                    }
431                                }
432                            }
433                            Err(error_details) => {
434                                return Err(RegentError::FailedTaskDryRun(format!(
435                                    "Unable to check service activity : {error_details}"
436                                )));
437                            }
438                        }
439                    }
440                    OsKind::Linux(_) => {
441                        if let Some(expected_state) = state {
442                            match service_is_active(host_handler, name).await {
443                                Ok(current_service_activity) => {
444                                    match (current_service_activity, expected_state) {
445                                        (
446                                            ServiceActivityStatus::Active,
447                                            ServiceExpectedStatus::Started,
448                                        )
449                                        | (
450                                            ServiceActivityStatus::Inactive,
451                                            ServiceExpectedStatus::Stopped,
452                                        ) => {}
453                                        (
454                                            ServiceActivityStatus::Active,
455                                            ServiceExpectedStatus::Stopped,
456                                        ) => {
457                                            remediations.push(Remediation::Service(
458                                                ServiceApiCall::from(
459                                                    ServiceModuleInternalApiCall::Stop(
460                                                        name.clone(),
461                                                    ),
462                                                    privilege.clone(),
463                                                ),
464                                            ));
465                                        }
466                                        (
467                                            ServiceActivityStatus::Inactive,
468                                            ServiceExpectedStatus::Started,
469                                        ) => {
470                                            remediations.push(Remediation::Service(
471                                                ServiceApiCall::from(
472                                                    ServiceModuleInternalApiCall::Start(
473                                                        name.clone(),
474                                                    ),
475                                                    privilege.clone(),
476                                                ),
477                                            ));
478                                        }
479                                        (
480                                            ServiceActivityStatus::ServiceNotFound,
481                                            _expected_state,
482                                        ) => {
483                                            return Ok(
484                                                AttributeComplianceAssessment::NonCompliantFatal(
485                                                    "service not found".to_string(),
486                                                ),
487                                            );
488                                        }
489                                        (
490                                            ServiceActivityStatus::UnsupportedCase(details),
491                                            _expected_state,
492                                        ) => {
493                                            return Ok(
494                                                AttributeComplianceAssessment::NonCompliantFatal(
495                                                    details.to_string(),
496                                                ),
497                                            );
498                                        }
499                                    }
500                                }
501                                Err(error_details) => {
502                                    return Err(RegentError::FailedTaskDryRun(format!(
503                                        "Unable to check service activity : {error_details}"
504                                    )));
505                                }
506                            }
507                        }
508
509                        match service_is_enabled(host_handler, name).await {
510                            Ok(current_service_enabling) => {
511                                match (current_service_enabling, *enabled) {
512                                    (ServiceEnablingStatus::Enabled, true)
513                                    | (ServiceEnablingStatus::Disabled, false) => {}
514                                    (ServiceEnablingStatus::Enabled, false) => {
515                                        remediations.push(Remediation::Service(
516                                            ServiceApiCall::from(
517                                                ServiceModuleInternalApiCall::Disable(name.clone()),
518                                                privilege.clone(),
519                                            ),
520                                        ));
521                                        remediations.push(Remediation::Service(
522                                            ServiceApiCall::from(
523                                                ServiceModuleInternalApiCall::Enable(name.clone()),
524                                                privilege.clone(),
525                                            ),
526                                        ));
527                                    }
528                                    (ServiceEnablingStatus::Disabled, true) => {
529                                        remediations.push(Remediation::Service(
530                                            ServiceApiCall::from(
531                                                ServiceModuleInternalApiCall::Enable(name.clone()),
532                                                privilege.clone(),
533                                            ),
534                                        ));
535                                    }
536                                    (ServiceEnablingStatus::ServiceNotFound, _expected_state) => {
537                                        return Ok(
538                                            AttributeComplianceAssessment::NonCompliantFatal(
539                                                "service not found".to_string(),
540                                            ),
541                                        );
542                                    }
543                                    (
544                                        ServiceEnablingStatus::UnsupportedCase(details),
545                                        _expected_state,
546                                    ) => {
547                                        return Ok(
548                                            AttributeComplianceAssessment::NonCompliantFatal(
549                                                details.to_string(),
550                                            ),
551                                        );
552                                    }
553                                }
554                            }
555                            Err(error_details) => {
556                                return Err(RegentError::FailedTaskDryRun(format!(
557                                    "Unable to check service activity : {error_details}"
558                                )));
559                            }
560                        }
561                    }
562                    OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {
563                        return Err(RegentError::FailedDryRunEvaluation(format!(
564                            "Service management is not supported on {:?}",
565                            os_kind
566                        )));
567                    }
568                }
569            }
570            Self::Action { name, action } => match os_kind {
571                #[cfg(feature = "windows")]
572                OsKind::Windows(_) => match &action {
573                    ServiceAction::Restarted => {
574                        remediations.push(Remediation::Service(ServiceApiCall::from(
575                            ServiceModuleInternalApiCall::Restart(name.clone()),
576                            privilege.clone(),
577                        )));
578                    }
579                    ServiceAction::Reloaded => {
580                        remediations.push(Remediation::Service(ServiceApiCall::from(
581                            ServiceModuleInternalApiCall::Reload(name.clone()),
582                            privilege.clone(),
583                        )));
584                    }
585                },
586                OsKind::Linux(_) => match &action {
587                    ServiceAction::Restarted => {
588                        remediations.push(Remediation::Service(ServiceApiCall::from(
589                            ServiceModuleInternalApiCall::Restart(name.clone()),
590                            privilege.clone(),
591                        )));
592                    }
593                    ServiceAction::Reloaded => {
594                        remediations.push(Remediation::Service(ServiceApiCall::from(
595                            ServiceModuleInternalApiCall::Reload(name.clone()),
596                            privilege.clone(),
597                        )));
598                    }
599                },
600                OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {
601                    return Err(RegentError::FailedDryRunEvaluation(format!(
602                        "Service management is not supported on {:?}",
603                        os_kind
604                    )));
605                }
606            },
607        }
608
609        if remediations.is_empty() {
610            Ok(AttributeComplianceAssessment::Compliant)
611        } else {
612            Ok(AttributeComplianceAssessment::NonCompliant(
613                RemediationsList::from(remediations)?,
614            ))
615        }
616    }
617}
618
619#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
620#[serde(rename_all = "PascalCase")]
621pub enum ServiceModuleInternalApiCall {
622    Start(String),
623    Stop(String),
624    Restart(String),
625    Reload(String),
626    Enable(String),
627    Disable(String),
628}
629
630impl std::fmt::Display for ServiceModuleInternalApiCall {
631    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
632        match self {
633            ServiceModuleInternalApiCall::Start(s) => write!(f, "start {}", s),
634            ServiceModuleInternalApiCall::Stop(s) => write!(f, "stop {}", s),
635            ServiceModuleInternalApiCall::Restart(s) => write!(f, "restart {}", s),
636            ServiceModuleInternalApiCall::Reload(s) => write!(f, "reload {}", s),
637            ServiceModuleInternalApiCall::Enable(s) => write!(f, "enable {}", s),
638            ServiceModuleInternalApiCall::Disable(s) => write!(f, "disable {}", s),
639        }
640    }
641}
642
643#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
644pub struct ServiceApiCall {
645    pub api_call: ServiceModuleInternalApiCall,
646    privilege: Privilege,
647}
648
649impl ServiceApiCall {
650    pub fn display(&self) -> String {
651        match &self.api_call {
652            ServiceModuleInternalApiCall::Start(s) => format!("Start service {}", s),
653            ServiceModuleInternalApiCall::Stop(s) => format!("Stop service {}", s),
654            ServiceModuleInternalApiCall::Restart(s) => format!("Restart service {}", s),
655            ServiceModuleInternalApiCall::Reload(s) => format!("Reload service {}", s),
656            ServiceModuleInternalApiCall::Enable(s) => format!("Enable service {}", s),
657            ServiceModuleInternalApiCall::Disable(s) => format!("Disable service {}", s),
658        }
659    }
660
661    fn from(api_call: ServiceModuleInternalApiCall, privilege: Privilege) -> ServiceApiCall {
662        ServiceApiCall {
663            api_call,
664            privilege,
665        }
666    }
667}
668
669impl Check for ServiceApiCall {
670    fn check(&self) -> Result<(), RegentError> {
671        Ok(())
672    }
673
674    fn check_host_compatibility(
675        &self,
676        host_properties: &HostProperties,
677    ) -> Result<(), RegentError> {
678        use crate::hosts::properties::InitSystem;
679        match host_properties.os_kind() {
680            OsKind::Linux(linux_specifics) => match linux_specifics.init_system {
681                InitSystem::Systemd => Ok(()),
682                InitSystem::Unknown => Err(RegentError::IncompatibleHost(
683                    "systemctl requires systemd but init system could not be detected".to_string(),
684                )),
685            },
686            #[cfg(feature = "windows")]
687            OsKind::Windows(_) => Ok(()),
688            incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
689                "Host is {:?} but service management is only supported on Linux with systemd or Windows (with windows feature)",
690                incompatible_os_kind
691            ))),
692        }
693    }
694}
695
696impl<Handler: HostHandler> ReachCompliance<Handler> for ServiceApiCall {
697    async fn call(
698        &self,
699        host_handler: &mut Handler,
700        host_properties: &Option<HostProperties>,
701        _optional_secret_provider: &Option<SecretProvidersPool>,
702    ) -> Result<InternalApiCallOutcome, RegentError> {
703        if let Some(props) = host_properties {
704            self.check_host_compatibility(props)?;
705        }
706
707        // Determine the effective OS kind - assume Linux if HostProperties is None
708        let os_kind = host_properties
709            .as_ref()
710            .map(|props| props.os_kind())
711            .unwrap_or(&OsKind::Linux(LinuxSpecifics {
712                linux_flavor: LinuxFlavor::Debian,
713                init_system: InitSystem::Systemd,
714            }));
715
716        match os_kind {
717            #[cfg(feature = "windows")]
718            OsKind::Windows(_) => {
719                let cmd = match &self.api_call {
720                    ServiceModuleInternalApiCall::Start(s) => format!("net start {}", s),
721                    ServiceModuleInternalApiCall::Stop(s) => format!("net stop {}", s),
722                    ServiceModuleInternalApiCall::Restart(s) => {
723                        // Windows doesn't have a direct restart command, we stop then start
724                        format!("net stop {} && net start {}", s, s)
725                    }
726                    ServiceModuleInternalApiCall::Reload(s) => {
727                        // Windows doesn't have a direct reload command
728                        // This might not be supported for all services
729                        format!("sc control {} 128", s) // Sends a reload parameter, but not all services support this
730                    }
731                    ServiceModuleInternalApiCall::Enable(s) => {
732                        format!("sc config {} start= auto", s)
733                    }
734                    ServiceModuleInternalApiCall::Disable(s) => {
735                        format!("sc config {} start= disabled", s)
736                    }
737                };
738
739                let result = host_handler.run_windows_command(&cmd).await;
740
741                match result {
742                    Ok(result) => {
743                        if result.return_code == 0 {
744                            Ok(InternalApiCallOutcome::Success(None))
745                        } else {
746                            Ok(InternalApiCallOutcome::Failure(format!(
747                                "RC: {}, STDOUT: {}, STDERR: {}",
748                                result.return_code, result.stdout, result.stderr
749                            )))
750                        }
751                    }
752                    Err(e) => Ok(InternalApiCallOutcome::Failure(format!(
753                        "Command execution failed: {:?}",
754                        e
755                    ))),
756                }
757            }
758            OsKind::Linux(_) => {
759                let cmd = match &self.api_call {
760                    ServiceModuleInternalApiCall::Start(s) => format!("systemctl start {}", s),
761                    ServiceModuleInternalApiCall::Stop(s) => format!("systemctl stop {}", s),
762                    ServiceModuleInternalApiCall::Restart(s) => format!("systemctl restart {}", s),
763                    ServiceModuleInternalApiCall::Reload(s) => format!("systemctl reload {}", s),
764                    ServiceModuleInternalApiCall::Enable(s) => format!("systemctl enable {}", s),
765                    ServiceModuleInternalApiCall::Disable(s) => format!("systemctl disable {}", s),
766                };
767
768                let result = host_handler.run_command(&cmd, &self.privilege).await;
769
770                match result {
771                    Ok(result) => {
772                        if result.return_code == 0 {
773                            Ok(InternalApiCallOutcome::Success(None))
774                        } else {
775                            Ok(InternalApiCallOutcome::Failure(format!(
776                                "RC: {}, STDOUT: {}, STDERR: {}",
777                                result.return_code, result.stdout, result.stderr
778                            )))
779                        }
780                    }
781                    Err(e) => Ok(InternalApiCallOutcome::Failure(format!(
782                        "Command execution failed: {:?}",
783                        e
784                    ))),
785                }
786            }
787            OsKind::FreeBsd(_) | OsKind::MacOs(_) | OsKind::Unknown => {
788                Err(RegentError::FailedDryRunEvaluation(format!(
789                    "Service management is not supported on {:?}",
790                    os_kind
791                )))
792            }
793        }
794    }
795}
796
797async fn service_is_active<Handler: HostHandler>(
798    host_handler: &mut Handler,
799    name: &str,
800) -> Result<ServiceActivityStatus, String> {
801    match host_handler
802        .run_command(&format!("systemctl is-active {}", name), &Privilege::None)
803        .await
804    {
805        Ok(r) => match r.return_code {
806            0 => Ok(ServiceActivityStatus::Active),
807            3 => Ok(ServiceActivityStatus::Inactive),
808            4 => Ok(ServiceActivityStatus::ServiceNotFound),
809            _ => Ok(ServiceActivityStatus::UnsupportedCase(format!("{:?}", r))),
810        },
811        Err(e) => Err(format!("Unable to check active state of {}: {:?}", name, e)),
812    }
813}
814
815async fn service_is_enabled<Handler: HostHandler>(
816    host_handler: &mut Handler,
817    name: &str,
818) -> Result<ServiceEnablingStatus, String> {
819    match host_handler
820        .run_command(&format!("systemctl is-enabled {}", name), &Privilege::None)
821        .await
822    {
823        Ok(r) => match r.return_code {
824            0 => Ok(ServiceEnablingStatus::Enabled),
825            1 | 3 => Ok(ServiceEnablingStatus::Disabled),
826            4 => Ok(ServiceEnablingStatus::ServiceNotFound),
827            _ => Ok(ServiceEnablingStatus::UnsupportedCase(format!("{:?}", r))),
828        },
829        Err(e) => Err(format!(
830            "Unable to check enabled state of {}: {:?}",
831            name, e
832        )),
833    }
834}
835
836#[cfg(feature = "windows")]
837async fn windows_service_is_active<Handler: HostHandler>(
838    host_handler: &mut Handler,
839    name: &str,
840) -> Result<ServiceActivityStatus, String> {
841    match host_handler
842        .run_windows_command(&format!("sc query {}", name))
843        .await
844    {
845        Ok(r) => {
846            // sc query returns 0 for success, but we need to parse the output
847            // The output contains "STATE" line which shows the service state
848            if r.return_code != 0 {
849                // Service might not exist or other error
850                if r.stdout.contains("does not exist") || r.stderr.contains("does not exist") {
851                    return Ok(ServiceActivityStatus::ServiceNotFound);
852                } else {
853                    return Ok(ServiceActivityStatus::UnsupportedCase(format!(
854                        "Unable to check service enabling status (command issue): {:?}",
855                        r
856                    )));
857                }
858            }
859
860            // Parse the output for service state
861            // Looking for lines like: "STATE              : 4  RUNNING"
862            let output = r.stdout.to_lowercase();
863            if output.contains("running") {
864                Ok(ServiceActivityStatus::Active)
865            } else if output.contains("stopped") || output.contains("pending") {
866                Ok(ServiceActivityStatus::Inactive)
867            } else {
868                Ok(ServiceActivityStatus::UnsupportedCase(format!(
869                    "Unable to check service activity (output parsing issue): {:?}",
870                    r
871                )))
872            }
873        }
874        Err(e) => Err(format!("Unable to check active state of {}: {:?}", name, e)),
875    }
876}
877
878#[cfg(feature = "windows")]
879async fn windows_service_is_enabled<Handler: HostHandler>(
880    host_handler: &mut Handler,
881    name: &str,
882) -> Result<ServiceEnablingStatus, String> {
883    match host_handler
884        .run_windows_command(&format!("sc qc {}", name))
885        .await
886    {
887        Ok(r) => {
888            // sc qc (query configuration) returns information about the service
889            // We need to look for the START_TYPE line
890            if r.return_code != 0 {
891                if r.stdout.contains("does not exist") || r.stderr.contains("does not exist") {
892                    return Ok(ServiceEnablingStatus::ServiceNotFound);
893                } else {
894                    return Ok(ServiceEnablingStatus::UnsupportedCase(format!(
895                        "Unable to check service enabling status (command issue): {:?}",
896                        r
897                    )));
898                }
899            }
900
901            // Parse the output for start type
902            // Looking for lines like: "START_TYPE       : 2   AUTO_START"
903            let output = r.stdout.to_lowercase();
904            if output.contains("auto_start") || output.contains("2") {
905                Ok(ServiceEnablingStatus::Enabled)
906            } else if output.contains("disabled") || output.contains("3") || output.contains("4") {
907                Ok(ServiceEnablingStatus::Disabled)
908            } else {
909                Ok(ServiceEnablingStatus::UnsupportedCase(format!(
910                    "Unable to check service enabling status (output parsing issue): {:?}",
911                    r
912                )))
913            }
914        }
915        Err(e) => Err(format!(
916            "Unable to check enabled state of {}: {:?}",
917            name, e
918        )),
919    }
920}
921
922enum ServiceActivityStatus {
923    Active,
924    Inactive,
925    ServiceNotFound,
926    UnsupportedCase(String),
927}
928
929enum ServiceEnablingStatus {
930    Enabled,
931    Disabled,
932    ServiceNotFound,
933    UnsupportedCase(String),
934}
935
936#[cfg(test)]
937mod tests {
938    use super::*;
939
940    #[test]
941    fn parsing_service_module_block_from_yaml_str() {
942        let raw = "---
943- Name: nginx
944  State: Started
945  Enabled: true
946
947- Name: nginx
948  State: Stopped
949  Enabled: false
950
951- Name: nginx
952  Action: Restarted
953
954- Name: nginx
955  Action: Reloaded
956
957- Name: nginx
958  Enabled: true
959        ";
960        let _: Vec<ServiceExpectedState> = yaml_serde::from_str(raw).unwrap();
961    }
962
963    #[test]
964    fn named_constructors_match_explicit_state() {
965        assert_eq!(
966            ServiceExpectedState::started_and_enabled("nginx"),
967            ServiceExpectedState::state("nginx", ServiceExpectedStatus::Started, true)
968        );
969        assert_eq!(
970            ServiceExpectedState::started_and_disabled("nginx"),
971            ServiceExpectedState::state("nginx", ServiceExpectedStatus::Started, false)
972        );
973        assert_eq!(
974            ServiceExpectedState::stopped_and_enabled("nginx"),
975            ServiceExpectedState::state("nginx", ServiceExpectedStatus::Stopped, true)
976        );
977        assert_eq!(
978            ServiceExpectedState::stopped_and_disabled("nginx"),
979            ServiceExpectedState::state("nginx", ServiceExpectedStatus::Stopped, false)
980        );
981        assert_eq!(
982            ServiceExpectedState::disabled("nginx"),
983            ServiceExpectedState::enabled("nginx", false)
984        );
985    }
986}