Skip to main content

hvctrl/vmware/
vmrest.rs

1// Copyright takubokudori.
2// This source code is licensed under the MIT or Apache-2.0 license.
3//! VMRest controller.
4use crate::{deserialize, exec_cmd, types::*};
5use reqwest::StatusCode;
6use serde::{Deserialize, Serialize};
7use std::{
8    io::Write,
9    process::Command,
10    time::{Duration, Instant},
11};
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
14pub enum VmRestPowerCommand {
15    On,
16    Off,
17    Shutdown,
18    Suspend,
19}
20
21impl VmRestPowerCommand {
22    pub fn to_command(&self) -> &'static str {
23        match self {
24            Self::On => "on",
25            Self::Off => "off",
26            Self::Shutdown => "shutdown",
27            Self::Suspend => "suspend",
28        }
29    }
30}
31
32impl ToString for VmRestPowerCommand {
33    fn to_string(&self) -> String { self.to_command().to_string() }
34}
35
36#[derive(Deserialize)]
37struct NicDevice {
38    index: i32,
39    #[serde(alias = "type")]
40    #[allow(dead_code)]
41    ty: String,
42    #[allow(dead_code)]
43    vmnet: String,
44    #[serde(alias = "macAddress")]
45    #[allow(dead_code)]
46    mac_address: String,
47}
48
49impl<T: AsRef<str>> From<T> for NicType {
50    fn from(s: T) -> Self {
51        match s.as_ref() {
52            "bridged" => Self::Bridge,
53            "nat" => Self::NAT,
54            "hostOnly" => Self::HostOnly,
55            "custom" => Self::Custom("".to_string()),
56            _ => panic!("Unknown type: {}", s.as_ref()),
57        }
58    }
59}
60
61#[derive(Clone, Debug)]
62pub struct VmRest {
63    executable_path: String,
64    url: String,
65    vm_id: Option<String>,
66    proxy: Option<String>,
67    encoding: String,
68    username: Option<String>,
69    password: Option<String>,
70}
71
72impl Default for VmRest {
73    fn default() -> Self { Self::new() }
74}
75
76impl VmRest {
77    pub fn new() -> Self {
78        Self {
79            executable_path: "vmrest".to_string(),
80            url: "http://127.0.0.1:8697".to_string(),
81            encoding: "utf-8".to_string(),
82            vm_id: None,
83            proxy: None,
84            username: None,
85            password: None,
86        }
87    }
88
89    impl_setter!(executable_path: String);
90
91    pub fn url<T: Into<String>>(&mut self, url: T) -> &mut Self {
92        self.url = url.into();
93        if !self.url.starts_with("http://") && self.url.starts_with("https://")
94        {
95            panic!("Invalid scheme specified in url: {}", self.url);
96        }
97        self
98    }
99
100    impl_setter!(@opt vm_id: String);
101    impl_setter!(@opt username: String);
102    impl_setter!(@opt password: String);
103    impl_setter!(@opt proxy: String);
104    impl_setter!(encoding: String);
105
106    /// Starts vmrest server.
107    pub fn start_vmrest_server(&mut self, port: Option<u16>) -> VmResult<()> {
108        let mut cmd = Command::new(&self.executable_path);
109        if let Some(port) = port {
110            cmd.args(&["-p", &port.to_string()]);
111        }
112        let (stdout, _) = exec_cmd(&mut cmd)?;
113        for d in stdout.lines() {
114            if let Some(url) = d.strip_prefix("Serving HTTP on ") {
115                self.url = format!("http://{}", url);
116                return Ok(());
117            }
118        }
119        vmerr!(Repr::Unknown("Failed to start a server".to_string()))
120    }
121
122    /// Creates a vmrest API server account using `vmrest -C`.
123    pub fn setup_user(&self, username: &str, password: &str) -> VmResult<()> {
124        match Command::new(&self.executable_path).arg("-C").spawn() {
125            Ok(mut x) => {
126                let stdin = x.stdin.as_mut().unwrap();
127                stdin
128                    .write_fmt(format_args!(
129                        "{}\n{}\n{}\n",
130                        username, password, password
131                    ))
132                    .unwrap();
133                match x.wait_with_output() {
134                    Ok(_) => Ok(()),
135                    Err(x) => vmerr!(ErrorKind::ExecutionFailed(x.to_string())),
136                }
137            }
138            Err(x) => vmerr!(ErrorKind::ExecutionFailed(x.to_string())),
139        }
140    }
141
142    fn execute(
143        &self,
144        v: reqwest::blocking::RequestBuilder,
145    ) -> VmResult<String> {
146        let v = v.header("Accept", "application/vnd.vmware.vmw.rest-v1+json");
147        let v = if let Some(x) = &self.username {
148            v.basic_auth(x, self.password.as_ref())
149        } else {
150            v
151        };
152        match v.send() {
153            Ok(x) => Self::handle_response(x, &self.encoding),
154            Err(x) => vmerr!(ErrorKind::ExecutionFailed(x.to_string())),
155        }
156    }
157
158    pub fn get_client(&self) -> VmResult<reqwest::blocking::Client> {
159        match self.proxy {
160            Some(ref x) => Ok(reqwest::blocking::Client::builder()
161                .proxy(reqwest::Proxy::http(x).unwrap())
162                .build()
163                .unwrap()),
164            None => Ok(reqwest::blocking::Client::new()),
165        }
166    }
167
168    fn handle_response(
169        resp: reqwest::blocking::Response,
170        encoding: &str,
171    ) -> VmResult<String> {
172        let is_success = resp.status() == StatusCode::OK;
173        let text = match resp.text_with_charset(encoding) {
174            Ok(x) => x,
175            Err(x) => {
176                return vmerr!(Repr::Unknown(format!(
177                    "Failed to convert error: {}",
178                    x.to_string()
179                )));
180            }
181        };
182        if is_success {
183            Ok(text)
184        } else {
185            Self::handle_error(text)
186        }
187    }
188
189    pub fn handle_error(s: String) -> VmResult<String> {
190        #[derive(Debug, Clone, Deserialize)]
191        struct VmRestFailedResponse {
192            #[serde(alias = "Code")]
193            code: i32,
194            #[serde(alias = "Message")]
195            message: String,
196        }
197
198        let ts = s.trim();
199        if ts == "404 page not found" {
200            return vmerr!(ErrorKind::UnsupportedCommand);
201        }
202        match serde_json::from_str::<VmRestFailedResponse>(&ts) {
203            Ok(x) => Err(Self::handle_json_error(&x.message)),
204            Err(_) => Ok(s),
205        }
206    }
207
208    fn handle_json_error(s: &str) -> VmError {
209        const RP: &str = "Redundant parameter: ";
210        const OOP: &str = "One of the parameters was invalid: ";
211        if let Some(s) = s.strip_prefix(RP) {
212            return VmError::from(ErrorKind::InvalidParameter(s.to_string()));
213        }
214        if let Some(s) = s.strip_prefix(OOP) {
215            return VmError::from(ErrorKind::InvalidParameter(s.to_string()));
216        }
217        match s {
218            "Authentication failed" => {
219                VmError::from(ErrorKind::AuthenticationFailed)
220            }
221            "The virtual machine is not powered on" => VmError::from(
222                ErrorKind::InvalidPowerState(VmPowerState::NotRunning),
223            ),
224            "The virtual network cannot be found" => {
225                VmError::from(ErrorKind::NetworkNotFound)
226            }
227            "The network adapter cannot be found" => {
228                VmError::from(ErrorKind::NetworkAdaptorNotFound)
229            }
230            _ => VmError::from(Repr::Unknown(format!("Unknown error: {}", s))),
231        }
232    }
233
234    fn serialize<T: Serialize>(o: &T) -> VmResult<String> {
235        match serde_json::to_string(o) {
236            Ok(x) => Ok(x),
237            Err(x) => vmerr!(ErrorKind::InvalidParameter(x.to_string())),
238        }
239    }
240
241    /// Gets the VM ID from the path.
242    pub fn get_vm_id_by_path(&self, path: &str) -> VmResult<String> {
243        let vms = self.get_vms()?;
244        for vm in vms {
245            if path == vm.path.as_deref().expect("Failed to get path") {
246                return Ok(vm.id.expect("Failed to get id"));
247            }
248        }
249        vmerr!(ErrorKind::VmNotFound)
250    }
251
252    fn get_vm_id(&self) -> VmResult<&str> {
253        self.vm_id
254            .as_deref()
255            .ok_or_else(|| VmError::from(ErrorKind::VmIsNotSpecified))
256    }
257
258    pub fn version(&self) -> VmResult<String> {
259        let cli = self.get_client()?;
260        let v = cli.get(&format!("{}/json/swagger.json", self.url));
261        let s = self.execute(v)?;
262
263        fn find<'a>(s: &'a str, pat: &str) -> VmResult<&'a str> {
264            match s.find(pat) {
265                Some(x) => Ok(&s[x + pat.len()..]),
266                None => vmerr!(ErrorKind::UnexpectedResponse(s.to_string())),
267            }
268        }
269        let s = find(&s, "description\"")?;
270        let s = find(s, "\"")?;
271        let m = s.find(',').unwrap();
272        Ok(s[..m - 1].to_string())
273    }
274
275    pub fn get_vms(&self) -> VmResult<Vec<Vm>> {
276        let cli = self.get_client()?;
277        let v = cli.get(&format!("{}/api/vms", self.url));
278        let s = self.execute(v)?;
279        deserialize(&s)
280    }
281
282    pub fn delete_vm(&self) -> VmResult<()> {
283        let cli = self.get_client()?;
284        let v =
285            cli.delete(&format!("{}/api/vms/{}", self.url, self.get_vm_id()?));
286        let s = self.execute(v)?;
287        deserialize(&s)
288    }
289
290    pub fn get_power_state(&self) -> VmResult<VmPowerState> {
291        let cli = self.get_client()?;
292        let v = cli.get(&format!(
293            "{}/api/vms/{}/power",
294            self.url,
295            self.get_vm_id()?
296        ));
297        let s = self.execute(v)?;
298        #[derive(Deserialize)]
299        struct Resp {
300            power_state: String,
301        }
302        let r: Resp = deserialize(&s)?;
303        match r.power_state.as_str() {
304            "poweredOn" => Ok(VmPowerState::Running),
305            "poweredOff" => Ok(VmPowerState::Stopped),
306            "suspended" => Ok(VmPowerState::Suspended),
307            x => vmerr!(ErrorKind::UnexpectedResponse(x.to_string())),
308        }
309    }
310
311    pub fn set_power_state(
312        &self,
313        state: &VmRestPowerCommand,
314    ) -> VmResult<VmPowerState> {
315        let cli = self.get_client()?;
316        let v = cli
317            .put(&format!("{}/api/vms/{}/power", self.url, self.get_vm_id()?))
318            .header("Content-Type", "application/vnd.vmware.vmw.rest-v1+json")
319            .body(state.to_command());
320        let s = self.execute(v)?;
321        #[derive(Deserialize)]
322        struct Resp {
323            power_state: String,
324        }
325        let r: Resp = deserialize(&s)?;
326        match r.power_state.as_str() {
327            "poweredOn" => Ok(VmPowerState::Running),
328            "poweredOff" => Ok(VmPowerState::Stopped),
329            "suspended" => Ok(VmPowerState::Suspended),
330            x => {
331                vmerr!(ErrorKind::UnexpectedResponse(format!(
332                    "set_power_state: {}",
333                    x
334                )))
335            }
336        }
337    }
338
339    pub fn get_ip_address(&self) -> VmResult<String> {
340        let cli = self.get_client()?;
341        let v =
342            cli.get(&format!("{}/api/vms/{}/ip", self.url, self.get_vm_id()?));
343        let s = self.execute(v)?;
344        #[derive(Deserialize)]
345        struct Resp {
346            ip: String,
347        }
348        let r: Resp = deserialize(&s)?;
349        Ok(r.ip)
350    }
351
352    pub fn list_nics(&self) -> VmResult<Vec<Nic>> {
353        let cli = self.get_client()?;
354        let v =
355            cli.get(&format!("{}/api/vms/{}/nic", self.url, self.get_vm_id()?));
356        let s = self.execute(v)?;
357
358        #[derive(Deserialize)]
359        struct NicDevices {
360            num: usize,
361            nics: Vec<NicDevice>,
362        }
363        let r: NicDevices = deserialize(&s)?;
364        assert_eq!(r.num, r.nics.len());
365        Ok(r.nics
366            .iter()
367            .map(|x| Nic {
368                id: Some(x.index.to_string()),
369                name: Some(x.vmnet.clone()),
370                ty: Some(x.ty.as_str().into()),
371                mac_address: Some(x.mac_address.clone()),
372            })
373            .collect())
374    }
375
376    pub fn create_nic(&self, ty: &NicType) -> VmResult<Nic> {
377        let cli = self.get_client()?;
378        #[derive(Serialize)]
379        struct Req {
380            #[serde(rename(serialize = "type"))]
381            ty: String,
382            vmnet: Option<String>,
383        }
384        let v = cli
385            .post(&format!("{}/api/vms/{}/nic", self.url, self.get_vm_id()?))
386            .header("Content-Type", "application/vnd.vmware.vmw.rest-v1+json")
387            .body(Self::serialize({
388                let (ty, vmnet) = match ty {
389                    NicType::NAT => ("nat".to_string(), None),
390                    NicType::Bridge => ("bridged".to_string(), None),
391                    NicType::HostOnly => ("hostonly".to_string(), None),
392                    NicType::Custom(x) => {
393                        ("custom".to_string(), Some(x.to_string()))
394                    }
395                };
396                &Req { ty, vmnet }
397            })?);
398
399        let s = self.execute(v)?;
400        let r: NicDevice = deserialize(&s)?;
401
402        Ok(Nic {
403            id: Some(r.index.to_string()),
404            name: Some(r.vmnet),
405            ty: Some(r.ty.into()),
406            mac_address: Some(r.mac_address),
407        })
408    }
409
410    pub fn update_nic(&self, index: i32, ty: &NicType) -> VmResult<()> {
411        let cli = self.get_client()?;
412        #[derive(Serialize)]
413        struct Req {
414            #[serde(rename(serialize = "type"))]
415            ty: String,
416            vmnet: Option<String>,
417        }
418        let v = cli
419            .put(&format!(
420                "{}/api/vms/{}/nic/{}",
421                self.url,
422                self.get_vm_id()?,
423                index
424            ))
425            .header("Content-Type", "application/vnd.vmware.vmw.rest-v1+json")
426            .body(Self::serialize({
427                let (ty, vmnet) = match ty {
428                    NicType::NAT => ("nat".to_string(), None),
429                    NicType::Bridge => ("bridged".to_string(), None),
430                    NicType::HostOnly => ("hostonly".to_string(), None),
431                    NicType::Custom(x) => {
432                        ("custom".to_string(), Some(x.to_string()))
433                    }
434                };
435                &Req { ty, vmnet }
436            })?);
437
438        let s = self.execute(v)?;
439        let r: NicDevice = deserialize(&s)?;
440        if r.index != index {
441            return vmerr!(ErrorKind::UnexpectedResponse(format!(
442                "{}",
443                r.index
444            )));
445        }
446        Ok(())
447    }
448
449    pub fn delete_nic(&self, index: i32) -> VmResult<()> {
450        let cli = self.get_client()?;
451        let v = cli.delete(&format!(
452            "{}/api/vms/{}/nic/{}",
453            self.url,
454            self.get_vm_id()?,
455            index
456        ));
457        self.execute(v)?;
458        Ok(())
459    }
460
461    pub fn list_shared_folders(&self) -> VmResult<Vec<SharedFolder>> {
462        let cli = self.get_client()?;
463        let v = cli.get(&format!(
464            "{}/api/vms/{}/sharedfolders",
465            self.url,
466            self.get_vm_id()?
467        ));
468        let s = self.execute(v)?;
469        #[derive(Deserialize)]
470        struct Resp {
471            folder_id: String,
472            host_path: String,
473            /// 0(R) or 4(RW)
474            flags: i32,
475        }
476        let r: Vec<Resp> = deserialize(&s)?;
477        Ok(r.iter()
478            .map(|x| SharedFolder {
479                id: Some(x.folder_id.clone()),
480                name: None,
481                guest_path: None,
482                host_path: Some(x.host_path.clone()),
483                is_readonly: x.flags != 4,
484            })
485            .collect())
486    }
487
488    pub fn mount_shared_folders(&self, shfs: &[&SharedFolder]) -> VmResult<()> {
489        let cli = self.get_client()?;
490        #[derive(Serialize)]
491        struct ShfReq {
492            folder_id: String,
493            host_path: String,
494            /// 0(R) or 4(RW)
495            flags: i32,
496        }
497        let v = cli
498            .post(&format!(
499                "{}/api/vms/{}/sharedfolders",
500                self.url,
501                self.get_vm_id()?
502            ))
503            .header("Content-Type", "application/vnd.vmware.vmw.rest-v1+json")
504            .body(Self::serialize(
505                &shfs
506                    .iter()
507                    .map(|x| ShfReq {
508                        folder_id: x.id.as_ref().unwrap().to_string(),
509                        host_path: x.host_path.as_ref().unwrap().to_string(),
510                        flags: if x.is_readonly { 0 } else { 4 },
511                    })
512                    .collect::<Vec<ShfReq>>(),
513            )?);
514        let _ = self.execute(v)?;
515        Ok(())
516    }
517
518    pub fn mount_shared_folder(
519        &self,
520        folder_id: &str,
521        host_path: &str,
522        is_readonly: bool,
523    ) -> VmResult<()> {
524        self.mount_shared_folders(&[&SharedFolder {
525            id: Some(folder_id.to_string()),
526            name: None,
527            guest_path: None,
528            host_path: Some(host_path.to_string()),
529            is_readonly,
530        }])
531    }
532
533    pub fn delete_shared_folder(&self, folder_id: &str) -> VmResult<()> {
534        let cli = self.get_client()?;
535        let v = cli.delete(&format!(
536            "{}/api/vms/{}/sharedfolders/{}",
537            self.url,
538            self.get_vm_id()?,
539            folder_id
540        ));
541        self.execute(v)?;
542        Ok(())
543    }
544
545    pub fn get_display_name(&self) -> VmResult<String> {
546        self.get_display_name_by_id(self.get_vm_id()?)
547    }
548
549    pub fn get_display_name_by_id(&self, id: &str) -> VmResult<String> {
550        for vm in self.get_vms()? {
551            if id == vm.id.as_deref().expect("Failed to get id") {
552                let path = vm.path.as_deref().unwrap();
553                return Self::get_display_name_from_vmx(path)
554                    .ok_or_else(|| VmError::from(ErrorKind::VmNotFound));
555            }
556        }
557        vmerr!(ErrorKind::VmNotFound)
558    }
559
560    fn get_display_name_from_vmx(path: &str) -> Option<String> {
561        use std::io::{BufRead, BufReader};
562        // Return `None` if the vmx file cannot be opened.
563        if let Ok(f) = std::fs::File::open(path) {
564            for l in BufReader::new(f).lines().flatten() {
565                if let Some(dn) = l.strip_prefix("displayName = \"") {
566                    if dn.is_empty() {
567                        // broken?
568                        return None;
569                    }
570                    let dn = &dn[..dn.len() - 1];
571                    return Some(dn.to_string());
572                }
573            }
574        }
575        None
576    }
577
578    fn is_running_result(&self) -> VmResult<()> {
579        if !self.get_power_state()?.is_running() {
580            vmerr!(ErrorKind::InvalidPowerState(VmPowerState::NotRunning))
581        } else {
582            Ok(())
583        }
584    }
585}
586
587fn expected_power_state(
588    res: VmResult<VmPowerState>,
589    expected: VmPowerState,
590) -> VmResult<()> {
591    match res {
592        Ok(x) if x == expected => Ok(()),
593        Ok(x) => vmerr!(ErrorKind::InvalidPowerState(x)),
594        Err(x) => Err(x),
595    }
596}
597
598impl VmCmd for VmRest {
599    fn list_vms(&self) -> VmResult<Vec<Vm>> { self.get_vms() }
600
601    fn set_vm_by_id(&mut self, id: &str) -> VmResult<()> {
602        for vm in self.get_vms()? {
603            if id == vm.id.as_deref().expect("Failed to get id") {
604                self.vm_id = vm.id;
605                return Ok(());
606            }
607        }
608        vmerr!(ErrorKind::VmNotFound)
609    }
610
611    /// `name` is the name of a VM as displayed in the GUI, not the `.vmx` file name.
612    fn set_vm_by_name(&mut self, name: &str) -> VmResult<()> {
613        for vm in self.get_vms()? {
614            let path = vm.path.as_deref().unwrap();
615            // Ignore if the vmx file cannot be opened.
616            if let Some(display_name) = Self::get_display_name_from_vmx(path) {
617                if name == display_name {
618                    self.vm_id = vm.id;
619                    return Ok(());
620                }
621            }
622        }
623        vmerr!(ErrorKind::VmNotFound)
624    }
625
626    fn set_vm_by_path(&mut self, path: &str) -> VmResult<()> {
627        self.vm_id = Some(self.get_vm_id_by_path(path)?);
628        Ok(())
629    }
630}
631
632impl PowerCmd for VmRest {
633    fn start(&self) -> VmResult<()> {
634        if self.get_power_state()?.is_running() {
635            return vmerr!(ErrorKind::InvalidPowerState(VmPowerState::Running));
636        }
637        expected_power_state(
638            self.set_power_state(&VmRestPowerCommand::On),
639            VmPowerState::Running,
640        )
641    }
642
643    fn stop<D: Into<Option<Duration>>>(&self, timeout: D) -> VmResult<()> {
644        let timeout = timeout.into();
645        let s = Instant::now();
646        self.is_running_result()?;
647        loop {
648            match self.set_power_state(&VmRestPowerCommand::Shutdown) {
649                Ok(VmPowerState::Stopped) => return Ok(()),
650                Ok(VmPowerState::Running) => { /* Does nothing */ }
651                Ok(x) => return vmerr!(ErrorKind::InvalidPowerState(x)),
652                Err(x) => return Err(x),
653            }
654
655            if let Some(timeout) = timeout {
656                if s.elapsed() >= timeout {
657                    return vmerr!(ErrorKind::Timeout);
658                }
659            }
660            std::thread::sleep(Duration::from_millis(200));
661        }
662    }
663
664    fn hard_stop(&self) -> VmResult<()> {
665        self.is_running_result()?;
666        expected_power_state(
667            self.set_power_state(&VmRestPowerCommand::Off),
668            VmPowerState::Stopped,
669        )
670    }
671
672    fn suspend(&self) -> VmResult<()> {
673        self.is_running_result()?;
674        expected_power_state(
675            self.set_power_state(&VmRestPowerCommand::Suspend),
676            VmPowerState::Suspended,
677        )
678    }
679
680    fn resume(&self) -> VmResult<()> { self.start() }
681
682    fn is_running(&self) -> VmResult<bool> {
683        Ok(self.get_power_state()? == VmPowerState::Running)
684    }
685
686    fn reboot<D: Into<Option<Duration>>>(&self, timeout: D) -> VmResult<()> {
687        self.is_running_result()?;
688        self.stop(timeout)?;
689        self.start()
690    }
691
692    fn hard_reboot(&self) -> VmResult<()> {
693        self.is_running_result()?;
694        let _ = self.hard_stop();
695        self.start()
696    }
697
698    fn pause(&self) -> VmResult<()> { vmerr!(ErrorKind::UnsupportedCommand) }
699
700    fn unpause(&self) -> VmResult<()> { vmerr!(ErrorKind::UnsupportedCommand) }
701}
702
703impl NicCmd for VmRest {
704    fn list_nics(&self) -> VmResult<Vec<Nic>> { VmRest::list_nics(self) }
705
706    fn add_nic(&self, nic: &Nic) -> VmResult<()> {
707        if let Some(ty) = &nic.ty {
708            VmRest::create_nic(self, ty)?;
709        } else {
710            return vmerr!(ErrorKind::InvalidParameter(
711                "ty is required".to_string()
712            ));
713        }
714        Ok(())
715    }
716
717    fn update_nic(&self, nic: &Nic) -> VmResult<()> {
718        if let (Some(index), Some(ty)) = (&nic.id, &nic.ty) {
719            VmRest::update_nic(self, index.parse().unwrap_or(0), ty)
720        } else {
721            vmerr!(ErrorKind::InvalidParameter(
722                "id and ty are required".to_string()
723            ))
724        }
725    }
726
727    fn remove_nic(&self, nic: &Nic) -> VmResult<()> {
728        if let Some(index) = &nic.id {
729            self.delete_nic(index.parse().unwrap_or(0))
730        } else {
731            vmerr!(ErrorKind::InvalidParameter("id is required".to_string()))
732        }
733    }
734}
735
736impl SharedFolderCmd for VmRest {
737    fn list_shared_folders(&self) -> VmResult<Vec<SharedFolder>> {
738        VmRest::list_shared_folders(self)
739    }
740
741    fn mount_shared_folder(&self, shfs: &SharedFolder) -> VmResult<()> {
742        VmRest::mount_shared_folders(self, &[shfs])
743    }
744
745    fn unmount_shared_folder(&self, shfs: &SharedFolder) -> VmResult<()> {
746        SharedFolderCmd::delete_shared_folder(self, shfs)
747    }
748
749    fn delete_shared_folder(&self, shfs: &SharedFolder) -> VmResult<()> {
750        if let Some(id) = &shfs.id {
751            Self::delete_shared_folder(self, id)
752        } else {
753            vmerr!(ErrorKind::InvalidParameter("id is required".to_string()))
754        }
755    }
756}