@@ -13,6 +13,7 @@ use crate::zone::{AddressRequest, IPADM, ZONE_PREFIX};
13
13
use camino:: { Utf8Path , Utf8PathBuf } ;
14
14
use ipnetwork:: IpNetwork ;
15
15
use omicron_common:: backoff;
16
+ use slog:: error;
16
17
use slog:: info;
17
18
use slog:: o;
18
19
use slog:: warn;
@@ -24,6 +25,16 @@ use crate::zone::MockZones as Zones;
24
25
#[ cfg( not( any( test, feature = "testing" ) ) ) ]
25
26
use crate :: zone:: Zones ;
26
27
28
+ /// Errors returned from methods for fetching SMF services and log files
29
+ #[ derive( thiserror:: Error , Debug ) ]
30
+ pub enum ServiceError {
31
+ #[ error( "I/O error" ) ]
32
+ Io ( #[ from] std:: io:: Error ) ,
33
+
34
+ #[ error( "Failed to run a command" ) ]
35
+ RunCommand ( #[ from] RunCommandError ) ,
36
+ }
37
+
27
38
/// Errors returned from [`RunningZone::run_cmd`].
28
39
#[ derive( thiserror:: Error , Debug ) ]
29
40
#[ error( "Error running command in zone '{zone}': {err}" ) ]
@@ -762,6 +773,128 @@ impl RunningZone {
762
773
pub fn links ( & self ) -> & Vec < Link > {
763
774
& self . inner . links
764
775
}
776
+
777
+ /// Return the running processes associated with all the SMF services this
778
+ /// zone is intended to run.
779
+ pub fn service_processes (
780
+ & self ,
781
+ ) -> Result < Vec < ServiceProcess > , ServiceError > {
782
+ let service_names = self . service_names ( ) ?;
783
+ let mut services = Vec :: with_capacity ( service_names. len ( ) ) ;
784
+ for service_name in service_names. into_iter ( ) {
785
+ let output = self . run_cmd ( [ "ptree" , "-s" , & service_name] ) ?;
786
+
787
+ // All Oxide SMF services currently run a single binary, though it
788
+ // may be run in a contract via `ctrun`. We don't care about that
789
+ // binary, but any others we _do_ want to collect data from.
790
+ for line in output. lines ( ) {
791
+ if line. contains ( "ctrun" ) {
792
+ continue ;
793
+ }
794
+ let line = line. trim ( ) ;
795
+ let mut parts = line. split_ascii_whitespace ( ) ;
796
+
797
+ // The first two parts should be the PID and the process binary
798
+ // path, respectively.
799
+ let Some ( pid_s) = parts. next ( ) else {
800
+ error ! (
801
+ self . inner. log,
802
+ "failed to get service PID from ptree output" ;
803
+ "service" => & service_name,
804
+ ) ;
805
+ continue ;
806
+ } ;
807
+ let Ok ( pid) = pid_s. parse ( ) else {
808
+ error ! (
809
+ self . inner. log,
810
+ "failed to parse service PID from ptree output" ;
811
+ "service" => & service_name,
812
+ "pid" => pid_s,
813
+ ) ;
814
+ continue ;
815
+ } ;
816
+ let Some ( path) = parts. next ( ) else {
817
+ error ! (
818
+ self . inner. log,
819
+ "failed to get service binary from ptree output" ;
820
+ "service" => & service_name,
821
+ ) ;
822
+ continue ;
823
+ } ;
824
+ let binary = Utf8PathBuf :: from ( path) ;
825
+
826
+ // Fetch any log files for this SMF service.
827
+ let Some ( ( log_file, rotated_log_files) ) = self . service_log_files ( & service_name) ? else {
828
+ error ! (
829
+ self . inner. log,
830
+ "failed to find log files for existing service" ;
831
+ "service_name" => & service_name,
832
+ ) ;
833
+ continue ;
834
+ } ;
835
+
836
+ services. push ( ServiceProcess {
837
+ service_name : service_name. clone ( ) ,
838
+ binary,
839
+ pid,
840
+ log_file,
841
+ rotated_log_files,
842
+ } ) ;
843
+ }
844
+ }
845
+ Ok ( services)
846
+ }
847
+
848
+ /// Return the names of the Oxide SMF services this zone is intended to run.
849
+ pub fn service_names ( & self ) -> Result < Vec < String > , ServiceError > {
850
+ const NEEDLES : [ & str ; 2 ] = [ "/oxide" , "/system/illumos" ] ;
851
+ let output = self . run_cmd ( & [ "svcs" , "-H" , "-o" , "fmri" ] ) ?;
852
+ Ok ( output
853
+ . lines ( )
854
+ . filter ( |line| NEEDLES . iter ( ) . any ( |needle| line. contains ( needle) ) )
855
+ . map ( |line| line. trim ( ) . to_string ( ) )
856
+ . collect ( ) )
857
+ }
858
+
859
+ /// Return any SMF log files associated with the named service.
860
+ ///
861
+ /// Given a named service, this returns a tuple of the latest or current log
862
+ /// file, and an array of any rotated log files. If the service does not
863
+ /// exist, or there are no log files, `None` is returned.
864
+ pub fn service_log_files (
865
+ & self ,
866
+ name : & str ,
867
+ ) -> Result < Option < ( Utf8PathBuf , Vec < Utf8PathBuf > ) > , ServiceError > {
868
+ let output = self . run_cmd ( & [ "svcs" , "-L" , name] ) ?;
869
+ let mut lines = output. lines ( ) ;
870
+ let Some ( current) = lines. next ( ) else {
871
+ return Ok ( None ) ;
872
+ } ;
873
+ // We need to prepend the zonepath root to get the path in the GZ. We
874
+ // can do this with `join()`, but that will _replace_ the path if the
875
+ // second one is absolute. So trim any prefixed `/` from each path.
876
+ let root = self . root ( ) ;
877
+ let current_log_file =
878
+ root. join ( current. trim ( ) . trim_start_matches ( '/' ) ) ;
879
+
880
+ // The rotated log files should have the same prefix as the current, but
881
+ // with an index appended. We'll search the parent directory for
882
+ // matching names, skipping the current file.
883
+ //
884
+ // See https://illumos.org/man/8/logadm for details on the naming
885
+ // conventions around these files.
886
+ let dir = current_log_file. parent ( ) . unwrap ( ) ;
887
+ let mut rotated_files = Vec :: new ( ) ;
888
+ for entry in dir. read_dir_utf8 ( ) ? {
889
+ let entry = entry?;
890
+ let path = entry. path ( ) ;
891
+ if path != current_log_file && path. starts_with ( & current_log_file) {
892
+ rotated_files
893
+ . push ( root. join ( path. strip_prefix ( "/" ) . unwrap_or ( path) ) ) ;
894
+ }
895
+ }
896
+ Ok ( Some ( ( current_log_file, rotated_files) ) )
897
+ }
765
898
}
766
899
767
900
impl Drop for RunningZone {
@@ -783,6 +916,21 @@ impl Drop for RunningZone {
783
916
}
784
917
}
785
918
919
+ /// A process running in the zone associated with an SMF service.
920
+ #[ derive( Clone , Debug ) ]
921
+ pub struct ServiceProcess {
922
+ /// The name of the SMF service.
923
+ pub service_name : String ,
924
+ /// The path of the binary in the process image.
925
+ pub binary : Utf8PathBuf ,
926
+ /// The PID of the process.
927
+ pub pid : u32 ,
928
+ /// The path for the current log file.
929
+ pub log_file : Utf8PathBuf ,
930
+ /// The paths for any rotated log files.
931
+ pub rotated_log_files : Vec < Utf8PathBuf > ,
932
+ }
933
+
786
934
/// Errors returned from [`InstalledZone::install`].
787
935
#[ derive( thiserror:: Error , Debug ) ]
788
936
pub enum InstallZoneError {
@@ -817,7 +965,7 @@ pub struct InstalledZone {
817
965
// NIC used for control plane communication.
818
966
control_vnic : Link ,
819
967
820
- // Nic used for bootstrap network communication
968
+ // NIC used for bootstrap network communication
821
969
bootstrap_vnic : Option < Link > ,
822
970
823
971
// OPTE devices for the guest network interfaces
0 commit comments