@@ -13,6 +13,7 @@ use crate::zone::{AddressRequest, IPADM, ZONE_PREFIX};
1313use camino:: { Utf8Path , Utf8PathBuf } ;
1414use ipnetwork:: IpNetwork ;
1515use omicron_common:: backoff;
16+ use slog:: error;
1617use slog:: info;
1718use slog:: o;
1819use slog:: warn;
@@ -24,6 +25,16 @@ use crate::zone::MockZones as Zones;
2425#[ cfg( not( any( test, feature = "testing" ) ) ) ]
2526use crate :: zone:: Zones ;
2627
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+
2738/// Errors returned from [`RunningZone::run_cmd`].
2839#[ derive( thiserror:: Error , Debug ) ]
2940#[ error( "Error running command in zone '{zone}': {err}" ) ]
@@ -762,6 +773,125 @@ impl RunningZone {
762773 pub fn links ( & self ) -> & Vec < Link > {
763774 & self . inner . links
764775 }
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+ let dir = current_log_file. parent ( ) . unwrap ( ) ;
884+ let mut rotated_files = Vec :: new ( ) ;
885+ for entry in dir. read_dir_utf8 ( ) ? {
886+ let entry = entry?;
887+ let path = entry. path ( ) ;
888+ if path != current_log_file && path. starts_with ( & current_log_file) {
889+ rotated_files
890+ . push ( root. join ( path. strip_prefix ( "/" ) . unwrap_or ( path) ) ) ;
891+ }
892+ }
893+ Ok ( Some ( ( current_log_file, rotated_files) ) )
894+ }
765895}
766896
767897impl Drop for RunningZone {
@@ -783,6 +913,21 @@ impl Drop for RunningZone {
783913 }
784914}
785915
916+ /// A process running in the zone associated with an SMF service.
917+ #[ derive( Clone , Debug ) ]
918+ pub struct ServiceProcess {
919+ /// The name of the SMF service.
920+ pub service_name : String ,
921+ /// The path of the binary in the process image.
922+ pub binary : Utf8PathBuf ,
923+ /// The PID of the process.
924+ pub pid : u32 ,
925+ /// The path for the current log file.
926+ pub log_file : Utf8PathBuf ,
927+ /// The paths for any rotated log files.
928+ pub rotated_log_files : Vec < Utf8PathBuf > ,
929+ }
930+
786931/// Errors returned from [`InstalledZone::install`].
787932#[ derive( thiserror:: Error , Debug ) ]
788933pub enum InstallZoneError {
@@ -817,7 +962,7 @@ pub struct InstalledZone {
817962 // NIC used for control plane communication.
818963 control_vnic : Link ,
819964
820- // Nic used for bootstrap network communication
965+ // NIC used for bootstrap network communication
821966 bootstrap_vnic : Option < Link > ,
822967
823968 // OPTE devices for the guest network interfaces
0 commit comments