microsoft/openvmm

Public

mirrored from https://github.com/microsoft/openvmmAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
vmgstool-v2.0.1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

hyperv/tools/hypestv/src/windows/mod.rs

379lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4#![cfg(windows)]
5
6mod completions;
7mod hyperv;
8mod rustyline_printer;
9mod vm;
10
11use anyhow::Context;
12use clap::CommandFactory;
13use clap::FromArgMatches;
14use clap::Parser;
15use clap::ValueEnum;
16use futures::StreamExt;
17use futures::executor::block_on;
18use hyperv::run_hvc;
19use mesh::rpc::RpcSend;
20use pal_async::DefaultDriver;
21use rustyline_printer::Printer;
22use std::fmt::Display;
23use std::path::PathBuf;
24use vm::Vm;
25
26#[derive(Parser)]
27#[clap(
28 disable_help_flag = true,
29 disable_version_flag = true,
30 no_binary_name = true,
31 max_term_width = 100,
32 help_template("{subcommands}")
33)]
34pub(crate) enum InteractiveCommand {
35 /// Select the active VM.
36 Select {
37 /// The VM's name.
38 name: String,
39 },
40
41 /// List all VMs.
42 List,
43
44 /// Detach from the active VM.
45 Detach,
46
47 /// Quit the interactive shell.
48 #[clap(visible_alias = "q")]
49 Quit,
50
51 #[clap(flatten)]
52 Vm(VmCommand),
53}
54
55#[derive(Parser)]
56pub(crate) enum VmCommand {
57 /// Start the VM.
58 Start,
59
60 /// Paravisor commands.
61 #[clap(subcommand, visible_alias = "pv")]
62 Paravisor(ParavisorCommand),
63
64 /// Power off the VM.
65 Kill {
66 /// Force powering off the VM via the HCS API.
67 ///
68 /// Without this flag, this command uses the Hyper-V WMI interface.
69 /// This may fail if the VM is in a transition state that prevents
70 /// powering off for whatever reason (usually due to Hyper-V bugs).
71 #[clap(short, long)]
72 force: bool,
73 },
74
75 /// Reset the VM.
76 Reset,
77
78 /// Send a request to the VM to shut it down.
79 Shutdown {
80 /// Reboot the VM instead of powering it off.
81 #[clap(long, short = 'r')]
82 reboot: bool,
83 /// Hibernate the VM instead of powering it off.
84 #[clap(long, short = 'h', conflicts_with = "reboot")]
85 hibernate: bool,
86 /// Tell the guest to force the power state transition.
87 #[clap(long, short = 'f')]
88 force: bool,
89 },
90
91 /// Gets or sets the serial output mode.
92 Serial {
93 /// The serial port to configure (1 = COM1, etc.).
94 port: Option<u32>,
95 /// The serial output mode.
96 mode: Option<SerialMode>,
97 },
98
99 /// Injects an NMI.
100 Nmi {
101 /// The target VTL.
102 #[clap(long, default_value = "0")]
103 vtl: u32,
104 },
105}
106
107#[derive(Parser)]
108pub(crate) struct InspectArgs {
109 /// Enumerate state recursively.
110 #[clap(short, long)]
111 recursive: bool,
112 /// The recursive depth limit.
113 #[clap(short, long, requires("recursive"))]
114 limit: Option<usize>,
115 /// Update the path with a new value.
116 #[clap(short, long, conflicts_with("recursive"))]
117 update: Option<String>,
118 /// The element path to inspect.
119 element: Option<String>,
120}
121
122#[derive(ValueEnum, Copy, Clone)]
123pub(crate) enum SerialMode {
124 /// The serial port is disconnected.
125 Off,
126 /// The serial port output is logged to standard output.
127 Log,
128 /// The serial port input and output are connected to a new terminal
129 /// emulator window.
130 Term,
131 // TODO: add Console mode for interactive console.
132}
133
134impl Display for SerialMode {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 f.pad(self.to_possible_value().unwrap().get_name())
137 }
138}
139
140#[derive(Parser)]
141pub(crate) enum ParavisorCommand {
142 /// Tell the paravisor to start the VM.
143 Start,
144
145 /// Get or set the output mode for paravisor kmsg logs.
146 Kmsg { mode: Option<LogMode> },
147
148 /// Inpsect paravisor state.
149 #[clap(visible_alias = "x")]
150 Inspect(InspectArgs),
151
152 /// Get or set the paravisor command line.
153 CommandLine { command_line: Option<String> },
154
155 /// Reload the paravisor.
156 Reload,
157}
158
159#[derive(ValueEnum, Copy, Clone)]
160pub enum LogMode {
161 /// The log output is disabled.
162 Off,
163 /// The log is written to standard output.
164 Log,
165 /// The log is written to a new terminal emulator window.
166 Term,
167}
168
169impl Display for LogMode {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 f.pad(self.to_possible_value().unwrap().get_name())
172 }
173}
174
175pub(crate) enum Request {
176 Prompt(mesh::rpc::Rpc<(), String>),
177 Inspect(mesh::rpc::Rpc<(InspectTarget, String), anyhow::Result<inspect::Node>>),
178 Command(mesh::rpc::Rpc<InteractiveCommand, anyhow::Result<bool>>),
179}
180
181pub(crate) enum InspectTarget {
182 Paravisor,
183}
184
185// DEVNOTE: this tool is not intended to have a stable interface for scripting,
186// so resist the temptation to add any kind of way to invoke commands directly
187// from the command line.
188#[derive(Parser)]
189struct CommandLine {
190 /// The initial VM name. Use select to change the active VM.
191 vm: Option<String>,
192
193 #[clap(long, hide(true))]
194 relay_console_path: Option<PathBuf>,
195
196 #[clap(long, hide(true))]
197 relay_console_title: Option<String>,
198}
199
200pub async fn main(driver: DefaultDriver) -> anyhow::Result<()> {
201 let command_line = CommandLine::parse();
202 if let Some(relay_console_path) = command_line.relay_console_path {
203 return console_relay::relay_console(
204 &relay_console_path,
205 command_line
206 .relay_console_title
207 .unwrap_or("Hypestv".to_owned())
208 .as_ref(),
209 );
210 }
211
212 let mut rl = rustyline::Editor::<_, rustyline::history::FileHistory>::with_config(
213 rustyline::Config::builder()
214 .completion_type(rustyline::CompletionType::List)
215 .build(),
216 )
217 .unwrap();
218
219 let printer = Printer::new(
220 rl.create_external_printer()
221 .context("failed to create external printer")?,
222 );
223
224 let mut vm = if let Some(name) = command_line.vm {
225 Some(Vm::new(driver.clone(), name, printer.clone())?)
226 } else {
227 None
228 };
229
230 let (send, mut recv) = mesh::channel();
231
232 rl.set_helper(Some(completions::OpenvmmRustylineEditor {
233 req: send.clone(),
234 }));
235
236 let history_file = {
237 const HISTORY_FILE: &str = ".hypestv_history";
238
239 // using a `None` to kick off the `.or()` chain in order to make
240 // it a bit easier to visually inspect the fallback chain.
241 let history_folder = None
242 .or_else(dirs::state_dir)
243 .or_else(dirs::data_local_dir)
244 .map(|path| path.join("hypestv"));
245
246 if let Some(history_folder) = history_folder {
247 if let Err(err) = std::fs::create_dir_all(&history_folder) {
248 eprintln!(
249 "could not create directory: {}: {}",
250 history_folder.display(),
251 err
252 )
253 }
254
255 Some(history_folder.join(HISTORY_FILE))
256 } else {
257 None
258 }
259 };
260
261 if let Some(history_file) = &history_file {
262 println!("restoring history from {}", history_file.display());
263 if rl.load_history(history_file).is_err() {
264 println!("could not find existing {}", history_file.display());
265 }
266 }
267
268 // Update the help template for each subcommand.
269 let mut template = InteractiveCommand::command();
270 for sc in template.get_subcommands_mut() {
271 *sc = sc
272 .clone()
273 .help_template("{about-with-newline}\n{usage-heading}\n {usage}\n\n{all-args}");
274 }
275
276 std::thread::spawn(move || {
277 while let Ok(prompt) = block_on(send.call(Request::Prompt, ())) {
278 let line = match rl.readline(&prompt) {
279 Ok(line) => line,
280 Err(rustyline::error::ReadlineError::Interrupted) => {
281 // On CTRL+C, ignore the current line
282 continue;
283 }
284 Err(_) => {
285 break;
286 }
287 };
288
289 let trimmed = line.trim();
290 if trimmed.is_empty() {
291 continue;
292 }
293 if let Err(err) = rl.add_history_entry(&line) {
294 eprintln!("error adding to history: {}", err);
295 }
296
297 match parse(&mut template, trimmed) {
298 Ok(cmd) => match block_on(send.call_failable(Request::Command, cmd)) {
299 Ok(true) => {}
300 Ok(false) => break,
301 Err(err) => {
302 eprintln!("{:#}", err);
303 }
304 },
305 Err(err) => {
306 err.print().unwrap();
307 }
308 }
309
310 if let Some(history_file) = &history_file {
311 rl.append_history(history_file).unwrap();
312 }
313 }
314 });
315
316 while let Some(request) = recv.next().await {
317 match request {
318 Request::Prompt(rpc) => rpc.handle_sync(|()| {
319 if let Some(vm) = &vm {
320 format!(
321 "{vm_name} [{state}]> ",
322 vm_name = vm.name(),
323 state = vm.state()
324 )
325 } else {
326 "> ".to_string()
327 }
328 }),
329 Request::Inspect(rpc) => {
330 let vm = &mut vm;
331 rpc.handle(async |(target, path)| {
332 vm.as_mut()
333 .context("no active VM")?
334 .handle_inspect(target, &path)
335 .await
336 })
337 .await
338 }
339 Request::Command(rpc) => {
340 rpc.handle(async |cmd| {
341 match cmd {
342 InteractiveCommand::Detach => {
343 vm = None;
344 }
345 InteractiveCommand::Select { name } => {
346 if Some(name.as_ref()) != vm.as_ref().map(|vm| vm.name()) {
347 let new_vm = Vm::new(driver.clone(), name, printer.clone())?;
348 vm = Some(new_vm);
349 }
350 }
351 InteractiveCommand::List => {
352 run_hvc(|cmd| cmd.arg("list")).context("failed to list VMs")?;
353 }
354 InteractiveCommand::Vm(cmd) => {
355 vm.as_mut()
356 .context("no active VM")?
357 .handle_command(cmd)
358 .await?;
359 }
360 InteractiveCommand::Quit => {
361 return Ok(false);
362 }
363 }
364 Ok(true)
365 })
366 .await
367 }
368 }
369 }
370
371 Ok(())
372}
373
374fn parse(template: &mut clap::Command, line: &str) -> clap::error::Result<InteractiveCommand> {
375 let args = shell_words::split(line)
376 .map_err(|err| template.error(clap::error::ErrorKind::ValueValidation, err))?;
377 let matches = template.try_get_matches_from_mut(args)?;
378 InteractiveCommand::from_arg_matches(&matches).map_err(|err| err.format(template))
379}
380