1use 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 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 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 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 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 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 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 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 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 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) => { }
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}