-
Notifications
You must be signed in to change notification settings - Fork 40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[sled-agent] Add support for Support Bundle command execution #7011
Conversation
Example output from the two endpoints below. The goal here is to have nexus collect this raw data and dump it out to a file that will end up in the final support bundle.
|
.into_iter() | ||
.map(|cmd| cmd.get_output()) | ||
.collect::<Vec<_>>() | ||
.as_slice() | ||
.join("\n\n"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we joining these with \n\n
when we aren't doing so for the zoneadm command?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the spacing between multiple commands as the ipadm variant currently runs 3 commands. I can change it to a single new line if you would like, I just thought giving the output some space was easier on the eyes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, gotcha, didn't realize the outputs were also different here (Vec of Results vs one Result). This seems fine!
sled-agent/src/support_bundle.rs
Outdated
let mut command = Command::new("sleep"); | ||
command.env_clear().arg("10"); | ||
|
||
match execute_command_with_timeout(command, Duration::from_secs(5)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Can we use smaller timeouts here? just would be nice to shorten the latency of this test to < 5 seconds
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do
#[tokio::test] | ||
async fn test_command_stdout_is_correct() { | ||
let mut command = Command::new("echo"); | ||
command.env_clear().arg("No Cables. No Assembly. Just Cloud."); | ||
|
||
let res = execute_command_with_timeout(command, Duration::from_secs(5)) | ||
.await | ||
.unwrap(); | ||
let expected_output = | ||
"No Cables. No Assembly. Just Cloud.\n".to_string(); | ||
assert_eq!(expected_output, res.stdio); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_command_stderr_is_correct() { | ||
let mut command = Command::new("bash"); | ||
command.env_clear().args(&["-c", "echo oxide computer > /dev/stderr"]); | ||
|
||
let res = execute_command_with_timeout(command, Duration::from_secs(5)) | ||
.await | ||
.unwrap(); | ||
let expected_output = "oxide computer\n".to_string(); | ||
assert_eq!(expected_output, res.stdio); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_command_stdout_stderr_are_interleaved() { | ||
let mut command = Command::new("bash"); | ||
command.env_clear().args(&[ | ||
"-c", | ||
"echo one > /dev/stdout \ | ||
&& echo two > /dev/stderr \ | ||
&& echo three > /dev/stdout", | ||
]); | ||
|
||
let res = execute_command_with_timeout(command, Duration::from_secs(5)) | ||
.await | ||
.unwrap(); | ||
let expected_output = "one\ntwo\nthree\n".to_string(); | ||
assert_eq!(expected_output, res.stdio); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice tests!
sled-agent/src/support_bundle.rs
Outdated
} | ||
|
||
/// Takes a given `Command` and returns a lossy representation of the program | ||
// and it's arguments. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
teensy obnoxious copyedit: "it's" is an abbreviation for "it is", while "its" means "belonging to it)
// and it's arguments. | |
// and its arguments. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in c3b5c52
sled-agent/src/support_bundle.rs
Outdated
// Grab the command arguments | ||
let mut res = command | ||
.get_args() | ||
.map(|a| a.to_string_lossy().into()) | ||
.collect::<Vec<String>>(); | ||
|
||
// Grab the command itself | ||
res.insert(0, command.get_program().to_string_lossy().into()); | ||
res.join(" ") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: performance probably does not matter here, but we are doing this basically the least efficient way possible with regards to allocations:
to_string_lossy()
returns aCow
, because it allocates a newString
only if it's invalid UTF8, which is unlikely. but calling callingInto<String>
on thatCow
forces an allocation unconditionally.- we
collect
those strings, allocating aVec
. Vec::insert
at 0 forces the wholeVec
to be copied one index forward to make space for the inserted item --- if we instead usediter::chain()
or something to just put the program name first, we wouldn't have to shift the entire contents of the rest of theVec
's contentsVec::join
allocates a new string, so all the previous string allocations that we made during this function are now deallocated --- most of them didn't really need heap allocations
personally, i might consider doing something like this:
// Grab the command arguments | |
let mut res = command | |
.get_args() | |
.map(|a| a.to_string_lossy().into()) | |
.collect::<Vec<String>>(); | |
// Grab the command itself | |
res.insert(0, command.get_program().to_string_lossy().into()); | |
res.join(" ") | |
// Grab the command itself | |
let mut res = command.get_program().to_string_lossy().into(); | |
// Grab the command arguments | |
for arg in command.get_args() { | |
let arg = arg.to_string_lossy(); | |
write!(&mut res, " {arg}").expect("write! to strings never fails"); | |
} | |
res |
which is maybe less attractive than Vec::join
, but doesn't create a temporary allocation for each argument and for a Vec that all end up getting thrown away --- instead, everything is written directly to the returned String
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the detailed explanation! The insert at offset 0 was going to cause everything to shift and I wasn't too worried about the allocations here but overall your suggestion is much nicer and more efficient!!
cmd: Command, | ||
oneshot: oneshot::Sender<()>, | ||
) -> Result<SupportBundleCmdOutput, SupportBundleCmdError> { | ||
let cmd_string = command_to_string(&cmd); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, take it or leave it: AFAICT this is only ever called when formatting an error message, so perhaps we should call it lazily in the various map_err
closures, instead of doing it unconditionally at the top of the function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally intended to do this but Command::from
consumes the StdCommand
, but in the end we store it in the SupportBundleCmdOutput
struct anyways so it get's used regardless of an error.
sled-agent/src/support_bundle.rs
Outdated
res.join(" ") | ||
} | ||
|
||
/// Spawn a command asynchronously and collect it's output by interleaving |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Spawn a command asynchronously and collect it's output by interleaving | |
/// Spawn a command asynchronously and collect its output by interleaving |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in c3b5c52
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
This PR lays the groundwork for executing system commands and returning their output as raw data (both on success and failure) via the internal sled-agent HTTP API. Command execution collects both stdout and stdout in an interleaved fashion so that the command output is exactly how an operator would have seen it if they were sitting at the terminal.