1use crate::{
8 AzureHttpClient, Result,
9 ops::compute::ComputeOps,
10 types::compute::{
11 AccessUri, Disk, DiskCreateRequest, DiskListResult, DiskSku, DiskUpdateRequest,
12 GrantAccessData, VirtualMachine, VirtualMachineCreateRequest,
13 VirtualMachineInstanceViewResult, VirtualMachineListResult, VirtualMachineScaleSet,
14 VirtualMachineScaleSetCreateRequest, VirtualMachineScaleSetListResult,
15 VirtualMachineScaleSetVMInstanceIDs, VirtualMachineScaleSetVMListResult,
16 },
17};
18
19pub struct ComputeClient<'a> {
24 ops: ComputeOps<'a>,
25 client: &'a AzureHttpClient,
26}
27
28impl<'a> ComputeClient<'a> {
29 pub(crate) fn new(client: &'a AzureHttpClient) -> Self {
31 Self {
32 ops: ComputeOps::new(client),
33 client,
34 }
35 }
36
37 pub async fn list_vms(&self, resource_group_name: &str) -> Result<VirtualMachineListResult> {
39 self.ops
40 .list_vms(self.client.subscription_id(), resource_group_name)
41 .await
42 }
43
44 pub async fn get_vm(&self, resource_group_name: &str, vm_name: &str) -> Result<VirtualMachine> {
46 self.ops
47 .get_vm(
48 self.client.subscription_id(),
49 resource_group_name,
50 vm_name,
51 "",
52 )
53 .await
54 }
55
56 pub async fn get_vm_expanded(
58 &self,
59 resource_group_name: &str,
60 vm_name: &str,
61 expand: &str,
62 ) -> Result<VirtualMachine> {
63 self.ops
64 .get_vm(
65 self.client.subscription_id(),
66 resource_group_name,
67 vm_name,
68 expand,
69 )
70 .await
71 }
72
73 pub async fn create_vm(
75 &self,
76 resource_group_name: &str,
77 vm_name: &str,
78 body: &VirtualMachineCreateRequest,
79 ) -> Result<VirtualMachine> {
80 self.ops
81 .create_vm(
82 self.client.subscription_id(),
83 resource_group_name,
84 vm_name,
85 body,
86 )
87 .await
88 }
89
90 pub async fn delete_vm(&self, resource_group_name: &str, vm_name: &str) -> Result<()> {
92 self.ops
93 .delete_vm(self.client.subscription_id(), resource_group_name, vm_name)
94 .await
95 }
96
97 pub async fn start_vm(&self, resource_group_name: &str, vm_name: &str) -> Result<()> {
99 self.ops
100 .start_vm(self.client.subscription_id(), resource_group_name, vm_name)
101 .await
102 }
103
104 pub async fn stop_vm(&self, resource_group_name: &str, vm_name: &str) -> Result<()> {
106 self.ops
107 .stop_vm(self.client.subscription_id(), resource_group_name, vm_name)
108 .await
109 }
110
111 pub async fn deallocate_vm(&self, resource_group_name: &str, vm_name: &str) -> Result<()> {
113 self.ops
114 .deallocate_vm(self.client.subscription_id(), resource_group_name, vm_name)
115 .await
116 }
117
118 pub async fn restart_vm(&self, resource_group_name: &str, vm_name: &str) -> Result<()> {
120 self.ops
121 .restart_vm(self.client.subscription_id(), resource_group_name, vm_name)
122 .await
123 }
124
125 pub async fn get_instance_view(
127 &self,
128 resource_group_name: &str,
129 vm_name: &str,
130 ) -> Result<VirtualMachineInstanceViewResult> {
131 self.ops
132 .get_instance_view(self.client.subscription_id(), resource_group_name, vm_name)
133 .await
134 }
135
136 pub async fn list_vmss(
140 &self,
141 resource_group_name: &str,
142 ) -> Result<VirtualMachineScaleSetListResult> {
143 self.ops
144 .list_vmss(self.client.subscription_id(), resource_group_name)
145 .await
146 }
147
148 pub async fn get_vmss(
150 &self,
151 resource_group_name: &str,
152 vmss_name: &str,
153 ) -> Result<VirtualMachineScaleSet> {
154 self.ops
155 .get_vmss(
156 self.client.subscription_id(),
157 resource_group_name,
158 vmss_name,
159 )
160 .await
161 }
162
163 pub async fn create_vmss(
165 &self,
166 resource_group_name: &str,
167 vmss_name: &str,
168 body: &VirtualMachineScaleSetCreateRequest,
169 ) -> Result<VirtualMachineScaleSet> {
170 self.ops
171 .create_vmss(
172 self.client.subscription_id(),
173 resource_group_name,
174 vmss_name,
175 body,
176 )
177 .await
178 }
179
180 pub async fn delete_vmss(&self, resource_group_name: &str, vmss_name: &str) -> Result<()> {
182 self.ops
183 .delete_vmss(
184 self.client.subscription_id(),
185 resource_group_name,
186 vmss_name,
187 )
188 .await
189 }
190
191 pub async fn list_vmss_instances(
193 &self,
194 resource_group_name: &str,
195 vmss_name: &str,
196 ) -> Result<VirtualMachineScaleSetVMListResult> {
197 self.ops
198 .list_vmss_instances(
199 self.client.subscription_id(),
200 resource_group_name,
201 vmss_name,
202 )
203 .await
204 }
205
206 pub async fn start_vmss_instances(
208 &self,
209 resource_group_name: &str,
210 vmss_name: &str,
211 instance_ids: &VirtualMachineScaleSetVMInstanceIDs,
212 ) -> Result<()> {
213 self.ops
214 .start_vmss_instances(
215 self.client.subscription_id(),
216 resource_group_name,
217 vmss_name,
218 instance_ids,
219 )
220 .await
221 }
222
223 pub async fn stop_vmss_instances(
225 &self,
226 resource_group_name: &str,
227 vmss_name: &str,
228 instance_ids: &VirtualMachineScaleSetVMInstanceIDs,
229 ) -> Result<()> {
230 self.ops
231 .stop_vmss_instances(
232 self.client.subscription_id(),
233 resource_group_name,
234 vmss_name,
235 instance_ids,
236 )
237 .await
238 }
239
240 pub async fn list_disks(&self, resource_group_name: &str) -> Result<DiskListResult> {
244 self.ops
245 .list_disks(self.client.subscription_id(), resource_group_name)
246 .await
247 }
248
249 pub async fn list_disks_in_subscription(&self) -> Result<DiskListResult> {
251 self.ops
252 .list_disks_in_subscription(self.client.subscription_id())
253 .await
254 }
255
256 pub async fn get_disk(&self, resource_group_name: &str, disk_name: &str) -> Result<Disk> {
258 self.ops
259 .get_disk(
260 self.client.subscription_id(),
261 resource_group_name,
262 disk_name,
263 )
264 .await
265 }
266
267 pub async fn create_disk(
269 &self,
270 resource_group_name: &str,
271 disk_name: &str,
272 body: &DiskCreateRequest,
273 ) -> Result<Disk> {
274 self.ops
275 .create_disk(
276 self.client.subscription_id(),
277 resource_group_name,
278 disk_name,
279 body,
280 )
281 .await
282 }
283
284 pub async fn delete_disk(&self, resource_group_name: &str, disk_name: &str) -> Result<()> {
286 self.ops
287 .delete_disk(
288 self.client.subscription_id(),
289 resource_group_name,
290 disk_name,
291 )
292 .await
293 }
294
295 pub async fn update_disk_sku(
306 &self,
307 resource_group_name: &str,
308 disk_name: &str,
309 sku_name: &str,
310 ) -> Result<Disk> {
311 let body = DiskUpdateRequest {
312 sku: Some(DiskSku {
313 name: Some(sku_name.to_string()),
314 ..Default::default()
315 }),
316 ..Default::default()
317 };
318 self.ops
319 .update_disk(
320 self.client.subscription_id(),
321 resource_group_name,
322 disk_name,
323 &body,
324 )
325 .await
326 }
327
328 pub async fn delete_snapshot(
330 &self,
331 subscription_id: &str,
332 resource_group_name: &str,
333 snapshot_name: &str,
334 ) -> Result<()> {
335 self.ops
336 .delete_snapshot(subscription_id, resource_group_name, snapshot_name)
337 .await
338 }
339
340 pub async fn grant_access(
346 &self,
347 resource_group_name: &str,
348 disk_name: &str,
349 body: &GrantAccessData,
350 ) -> Result<AccessUri> {
351 let sub = self.client.subscription_id();
352 let url = format!(
353 "https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/disks/{disk}/beginGetAccess?api-version=2024-07-01",
354 rg = urlencoding::encode(resource_group_name),
355 disk = urlencoding::encode(disk_name),
356 );
357
358 let body_bytes =
359 serde_json::to_vec(body).map_err(|e| crate::AzureError::InvalidResponse {
360 message: format!("Failed to serialize grant_access request: {e}"),
361 body: None,
362 })?;
363
364 let response = self.client.post(&url, &body_bytes).await?;
365 let location = response.location();
366 let response_bytes = response.error_for_status().await?.bytes().await?;
367
368 if !response_bytes.is_empty() {
370 return serde_json::from_slice(&response_bytes).map_err(|e| {
371 crate::AzureError::InvalidResponse {
372 message: format!("Failed to parse grant_access response: {e}"),
373 body: Some(String::from_utf8_lossy(&response_bytes).to_string()),
374 }
375 });
376 }
377
378 let poll_url = location.ok_or_else(|| crate::AzureError::InvalidResponse {
380 message: "grant_access returned 202 with no Location header".into(),
381 body: None,
382 })?;
383
384 for _ in 0..30 {
385 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
386 let poll_response = self.client.get(&poll_url).await?;
387 let poll_bytes = poll_response.error_for_status().await?.bytes().await?;
388 if !poll_bytes.is_empty() {
389 return serde_json::from_slice(&poll_bytes).map_err(|e| {
390 crate::AzureError::InvalidResponse {
391 message: format!("Failed to parse grant_access poll response: {e}"),
392 body: Some(String::from_utf8_lossy(&poll_bytes).to_string()),
393 }
394 });
395 }
396 }
397
398 Err(crate::AzureError::InvalidResponse {
399 message: "grant_access polling timed out after 30 attempts".into(),
400 body: None,
401 })
402 }
403
404 pub async fn revoke_access(&self, resource_group_name: &str, disk_name: &str) -> Result<()> {
406 self.ops
407 .revoke_access(
408 self.client.subscription_id(),
409 resource_group_name,
410 disk_name,
411 )
412 .await
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::types::compute::VirtualMachineProperties;
420
421 #[tokio::test]
422 async fn list_vms_returns_empty_list() {
423 let mut mock = crate::MockClient::new();
424 mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachines")
425 .returning_json(serde_json::json!({
426 "value": []
427 }));
428
429 let client = AzureHttpClient::from_mock(mock);
430 let compute = client.compute();
431 let result = compute.list_vms("my-rg").await.expect("list_vms failed");
432 assert!(result.value.is_empty());
433 assert!(result.next_link.is_none());
434 }
435
436 #[tokio::test]
437 async fn list_vms_returns_vm_with_fields() {
438 let mut mock = crate::MockClient::new();
439 mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachines")
440 .returning_json(serde_json::json!({
441 "value": [{
442 "id": "/subscriptions/sub/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachines/vm1",
443 "name": "vm1",
444 "type": "Microsoft.Compute/virtualMachines",
445 "location": "eastus",
446 "properties": {
447 "vmId": "abc-123",
448 "provisioningState": "Succeeded",
449 "hardwareProfile": { "vmSize": "Standard_B1s" }
450 }
451 }]
452 }));
453
454 let client = AzureHttpClient::from_mock(mock);
455 let compute = client.compute();
456 let result = compute.list_vms("my-rg").await.expect("list_vms failed");
457 assert_eq!(result.value.len(), 1);
458 let vm = &result.value[0];
459 assert_eq!(vm.name.as_deref(), Some("vm1"));
460 assert_eq!(vm.location.as_deref(), Some("eastus"));
461 assert_eq!(
462 vm.properties.as_ref().and_then(|p| p.vm_id.as_deref()),
463 Some("abc-123"),
464 );
465 assert_eq!(
466 vm.properties
467 .as_ref()
468 .and_then(|p| p.provisioning_state.as_deref()),
469 Some("Succeeded"),
470 );
471 }
472
473 #[tokio::test]
474 async fn get_vm_injects_subscription_id() {
475 let mut mock = crate::MockClient::new();
476 mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/my-vm")
477 .returning_json(serde_json::json!({
478 "id": "/subscriptions/sub/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/my-vm",
479 "name": "my-vm",
480 "type": "Microsoft.Compute/virtualMachines",
481 "location": "westus2",
482 "properties": {
483 "vmId": "vm-uuid",
484 "provisioningState": "Succeeded"
485 }
486 }));
487
488 let client = AzureHttpClient::from_mock(mock);
489 let compute = client.compute();
490 let vm = compute.get_vm("rg1", "my-vm").await.expect("get_vm failed");
491 assert_eq!(vm.name.as_deref(), Some("my-vm"));
492 assert_eq!(vm.location.as_deref(), Some("westus2"));
493 assert_eq!(
494 vm.properties.as_ref().and_then(|p| p.vm_id.as_deref()),
495 Some("vm-uuid"),
496 );
497 }
498
499 #[tokio::test]
500 async fn create_vm_sends_put_and_returns_vm() {
501 let mut mock = crate::MockClient::new();
502 mock.expect_put("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/new-vm")
503 .returning_json(serde_json::json!({
504 "name": "new-vm",
505 "location": "eastus",
506 "properties": {
507 "provisioningState": "Creating",
508 "hardwareProfile": { "vmSize": "Standard_B1s" }
509 }
510 }));
511
512 let client = AzureHttpClient::from_mock(mock);
513 let compute = client.compute();
514 let request = VirtualMachineCreateRequest {
515 location: "eastus".into(),
516 properties: Some(VirtualMachineProperties {
517 hardware_profile: Some(crate::types::compute::HardwareProfile {
518 vm_size: Some("Standard_B1s".into()),
519 }),
520 ..Default::default()
521 }),
522 ..Default::default()
523 };
524 let vm = compute
525 .create_vm("rg1", "new-vm", &request)
526 .await
527 .expect("create_vm failed");
528 assert_eq!(vm.name.as_deref(), Some("new-vm"));
529 assert_eq!(
530 vm.properties
531 .as_ref()
532 .and_then(|p| p.provisioning_state.as_deref()),
533 Some("Creating"),
534 );
535 }
536
537 #[tokio::test]
538 async fn delete_vm_sends_delete() {
539 let mut mock = crate::MockClient::new();
540 mock.expect_delete("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/dead-vm")
541 .returning_json(serde_json::json!(null));
542
543 let client = AzureHttpClient::from_mock(mock);
544 let compute = client.compute();
545 compute
546 .delete_vm("rg1", "dead-vm")
547 .await
548 .expect("delete_vm failed");
549 }
550
551 #[tokio::test]
552 async fn deallocate_vm_sends_post() {
553 let mut mock = crate::MockClient::new();
554 mock.expect_post("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/my-vm/deallocate")
555 .returning_json(serde_json::json!(null));
556
557 let client = AzureHttpClient::from_mock(mock);
558 let compute = client.compute();
559 compute
560 .deallocate_vm("rg1", "my-vm")
561 .await
562 .expect("deallocate_vm failed");
563 }
564
565 #[tokio::test]
566 async fn get_instance_view_returns_statuses() {
567 let mut mock = crate::MockClient::new();
568 mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/my-vm/instanceView")
569 .returning_json(serde_json::json!({
570 "statuses": [
571 {
572 "code": "ProvisioningState/succeeded",
573 "displayStatus": "Provisioning succeeded",
574 "level": "Info"
575 },
576 {
577 "code": "PowerState/running",
578 "displayStatus": "VM running",
579 "level": "Info"
580 }
581 ]
582 }));
583
584 let client = AzureHttpClient::from_mock(mock);
585 let compute = client.compute();
586 let iv = compute
587 .get_instance_view("rg1", "my-vm")
588 .await
589 .expect("get_instance_view failed");
590 assert_eq!(iv.statuses.len(), 2);
591 assert_eq!(
592 iv.statuses[0].code.as_deref(),
593 Some("ProvisioningState/succeeded")
594 );
595 assert_eq!(
596 iv.statuses[0].display_status.as_deref(),
597 Some("Provisioning succeeded")
598 );
599 assert_eq!(iv.statuses[1].code.as_deref(), Some("PowerState/running"));
600 }
601
602 #[tokio::test]
605 async fn list_vmss_returns_empty_list() {
606 let mut mock = crate::MockClient::new();
607 mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets")
608 .returning_json(serde_json::json!({ "value": [] }));
609
610 let client = AzureHttpClient::from_mock(mock);
611 let result = client
612 .compute()
613 .list_vmss("my-rg")
614 .await
615 .expect("list_vmss failed");
616 assert!(result.value.is_empty());
617 assert!(result.next_link.is_none());
618 }
619
620 #[tokio::test]
621 async fn list_vmss_returns_vmss_with_fields() {
622 let mut mock = crate::MockClient::new();
623 mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets")
624 .returning_json(serde_json::json!({
625 "value": [{
626 "id": "/subscriptions/sub/resourceGroups/my-rg/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss",
627 "name": "my-vmss",
628 "type": "Microsoft.Compute/virtualMachineScaleSets",
629 "location": "eastus",
630 "sku": { "name": "Standard_B1s", "tier": "Standard", "capacity": 1 },
631 "properties": {
632 "provisioningState": "Succeeded",
633 "uniqueId": "abc-123"
634 }
635 }]
636 }));
637
638 let client = AzureHttpClient::from_mock(mock);
639 let result = client
640 .compute()
641 .list_vmss("my-rg")
642 .await
643 .expect("list_vmss failed");
644 assert_eq!(result.value.len(), 1);
645 let vmss = &result.value[0];
646 assert_eq!(vmss.name.as_deref(), Some("my-vmss"));
647 assert_eq!(vmss.location.as_deref(), Some("eastus"));
648 assert_eq!(
649 vmss.sku.as_ref().and_then(|s| s.name.as_deref()),
650 Some("Standard_B1s")
651 );
652 assert_eq!(vmss.sku.as_ref().and_then(|s| s.capacity), Some(1));
653 assert_eq!(
654 vmss.properties
655 .as_ref()
656 .and_then(|p| p.provisioning_state.as_deref()),
657 Some("Succeeded"),
658 );
659 assert_eq!(
660 vmss.properties
661 .as_ref()
662 .and_then(|p| p.unique_id.as_deref()),
663 Some("abc-123"),
664 );
665 }
666
667 #[tokio::test]
668 async fn get_vmss_injects_subscription_id() {
669 let mut mock = crate::MockClient::new();
670 mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss")
671 .returning_json(serde_json::json!({
672 "name": "my-vmss",
673 "type": "Microsoft.Compute/virtualMachineScaleSets",
674 "location": "eastus",
675 "sku": { "name": "Standard_B1s", "tier": "Standard", "capacity": 2 },
676 "properties": {
677 "provisioningState": "Succeeded",
678 "uniqueId": "vmss-uuid"
679 }
680 }));
681
682 let client = AzureHttpClient::from_mock(mock);
683 let vmss = client
684 .compute()
685 .get_vmss("rg1", "my-vmss")
686 .await
687 .expect("get_vmss failed");
688 assert_eq!(vmss.name.as_deref(), Some("my-vmss"));
689 assert_eq!(vmss.sku.as_ref().and_then(|s| s.capacity), Some(2));
690 assert_eq!(
691 vmss.properties
692 .as_ref()
693 .and_then(|p| p.unique_id.as_deref()),
694 Some("vmss-uuid"),
695 );
696 }
697
698 #[tokio::test]
699 async fn create_vmss_sends_put_and_returns_vmss() {
700 let mut mock = crate::MockClient::new();
701 mock.expect_put("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/new-vmss")
702 .returning_json(serde_json::json!({
703 "name": "new-vmss",
704 "location": "eastus",
705 "sku": { "name": "Standard_B1s", "tier": "Standard", "capacity": 1 },
706 "properties": {
707 "provisioningState": "Creating"
708 }
709 }));
710
711 let client = AzureHttpClient::from_mock(mock);
712 let request = VirtualMachineScaleSetCreateRequest {
713 location: "eastus".into(),
714 sku: Some(crate::types::compute::Sku {
715 name: Some("Standard_B1s".into()),
716 capacity: Some(1),
717 ..Default::default()
718 }),
719 ..Default::default()
720 };
721 let vmss = client
722 .compute()
723 .create_vmss("rg1", "new-vmss", &request)
724 .await
725 .expect("create_vmss failed");
726 assert_eq!(vmss.name.as_deref(), Some("new-vmss"));
727 assert_eq!(
728 vmss.properties
729 .as_ref()
730 .and_then(|p| p.provisioning_state.as_deref()),
731 Some("Creating"),
732 );
733 }
734
735 #[tokio::test]
736 async fn delete_vmss_sends_delete() {
737 let mut mock = crate::MockClient::new();
738 mock.expect_delete("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/dead-vmss")
739 .returning_json(serde_json::json!(null));
740
741 let client = AzureHttpClient::from_mock(mock);
742 client
743 .compute()
744 .delete_vmss("rg1", "dead-vmss")
745 .await
746 .expect("delete_vmss failed");
747 }
748
749 #[tokio::test]
750 async fn list_vmss_instances_returns_instances() {
751 let mut mock = crate::MockClient::new();
752 mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/virtualMachines")
753 .returning_json(serde_json::json!({
754 "value": [{
755 "name": "my-vmss_0",
756 "instanceId": "0",
757 "location": "eastus",
758 "properties": {
759 "latestModelApplied": true,
760 "provisioningState": "Succeeded",
761 "vmId": "instance-uuid"
762 }
763 }]
764 }));
765
766 let client = AzureHttpClient::from_mock(mock);
767 let result = client
768 .compute()
769 .list_vmss_instances("rg1", "my-vmss")
770 .await
771 .expect("list_vmss_instances failed");
772 assert_eq!(result.value.len(), 1);
773 let inst = &result.value[0];
774 assert_eq!(inst.name.as_deref(), Some("my-vmss_0"));
775 assert_eq!(inst.instance_id.as_deref(), Some("0"));
776 assert_eq!(
777 inst.properties
778 .as_ref()
779 .and_then(|p| p.provisioning_state.as_deref()),
780 Some("Succeeded"),
781 );
782 assert_eq!(
783 inst.properties
784 .as_ref()
785 .and_then(|p| p.latest_model_applied),
786 Some(true),
787 );
788 }
789
790 #[tokio::test]
791 async fn stop_vmss_instances_sends_post() {
792 let mut mock = crate::MockClient::new();
793 mock.expect_post("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/poweroff")
794 .returning_json(serde_json::json!(null));
795
796 let client = AzureHttpClient::from_mock(mock);
797 let ids = VirtualMachineScaleSetVMInstanceIDs {
798 instance_ids: vec!["0".into()],
799 };
800 client
801 .compute()
802 .stop_vmss_instances("rg1", "my-vmss", &ids)
803 .await
804 .expect("stop_vmss_instances failed");
805 }
806
807 #[tokio::test]
808 async fn start_vmss_instances_sends_post() {
809 let mut mock = crate::MockClient::new();
810 mock.expect_post("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachineScaleSets/my-vmss/start")
811 .returning_json(serde_json::json!(null));
812
813 let client = AzureHttpClient::from_mock(mock);
814 let ids = VirtualMachineScaleSetVMInstanceIDs {
815 instance_ids: vec!["0".into(), "1".into()],
816 };
817 client
818 .compute()
819 .start_vmss_instances("rg1", "my-vmss", &ids)
820 .await
821 .expect("start_vmss_instances failed");
822 }
823
824 #[tokio::test]
827 async fn list_disks_returns_empty_list() {
828 let mut mock = crate::MockClient::new();
829 mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/my-rg/providers/Microsoft.Compute/disks")
830 .returning_json(serde_json::json!({ "value": [] }));
831
832 let client = AzureHttpClient::from_mock(mock);
833 let result = client
834 .compute()
835 .list_disks("my-rg")
836 .await
837 .expect("list_disks failed");
838 assert!(result.value.is_empty());
839 assert!(result.next_link.is_none());
840 }
841
842 #[tokio::test]
843 async fn list_disks_returns_disk_with_fields() {
844 let mut mock = crate::MockClient::new();
845 mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/my-rg/providers/Microsoft.Compute/disks")
846 .returning_json(serde_json::json!({
847 "value": [{
848 "id": "/subscriptions/sub/resourceGroups/my-rg/providers/Microsoft.Compute/disks/my-disk",
849 "name": "my-disk",
850 "type": "Microsoft.Compute/disks",
851 "location": "eastus",
852 "sku": { "name": "Standard_LRS" },
853 "properties": {
854 "diskSizeGB": 32,
855 "diskState": "Unattached",
856 "provisioningState": "Succeeded"
857 }
858 }]
859 }));
860
861 let client = AzureHttpClient::from_mock(mock);
862 let result = client
863 .compute()
864 .list_disks("my-rg")
865 .await
866 .expect("list_disks failed");
867 assert_eq!(result.value.len(), 1);
868 let disk = &result.value[0];
869 assert_eq!(disk.name.as_deref(), Some("my-disk"));
870 assert_eq!(disk.location.as_str(), "eastus");
871 assert_eq!(
872 disk.sku.as_ref().and_then(|s| s.name.as_deref()),
873 Some("Standard_LRS")
874 );
875 assert_eq!(
876 disk.properties.as_ref().and_then(|p| p.disk_size_gb),
877 Some(32)
878 );
879 assert_eq!(
880 disk.properties
881 .as_ref()
882 .and_then(|p| p.disk_state.as_deref()),
883 Some("Unattached"),
884 );
885 }
886
887 #[tokio::test]
888 async fn list_disks_in_subscription_returns_list() {
889 let mut mock = crate::MockClient::new();
890 mock.expect_get("/subscriptions/test-subscription-id/providers/Microsoft.Compute/disks")
891 .returning_json(serde_json::json!({
892 "value": [{
893 "name": "disk-a",
894 "location": "eastus",
895 "properties": {
896 "diskSizeGB": 64,
897 "diskState": "Unattached"
898 }
899 }]
900 }));
901
902 let client = AzureHttpClient::from_mock(mock);
903 let result = client
904 .compute()
905 .list_disks_in_subscription()
906 .await
907 .expect("list_disks_in_subscription failed");
908 assert_eq!(result.value.len(), 1);
909 assert_eq!(result.value[0].name.as_deref(), Some("disk-a"));
910 }
911
912 #[tokio::test]
913 async fn get_disk_returns_disk_fields() {
914 let mut mock = crate::MockClient::new();
915 mock.expect_get("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/disks/my-disk")
916 .returning_json(serde_json::json!({
917 "id": "/subscriptions/sub/resourceGroups/rg1/providers/Microsoft.Compute/disks/my-disk",
918 "name": "my-disk",
919 "location": "westus2",
920 "sku": { "name": "Premium_LRS", "tier": "Premium" },
921 "properties": {
922 "diskSizeGB": 128,
923 "diskState": "Unattached",
924 "provisioningState": "Succeeded",
925 "uniqueId": "disk-uuid-abc"
926 }
927 }));
928
929 let client = AzureHttpClient::from_mock(mock);
930 let disk = client
931 .compute()
932 .get_disk("rg1", "my-disk")
933 .await
934 .expect("get_disk failed");
935 assert_eq!(disk.name.as_deref(), Some("my-disk"));
936 assert_eq!(disk.location.as_str(), "westus2");
937 assert_eq!(
938 disk.sku.as_ref().and_then(|s| s.name.as_deref()),
939 Some("Premium_LRS")
940 );
941 assert_eq!(
942 disk.properties.as_ref().and_then(|p| p.disk_size_gb),
943 Some(128)
944 );
945 assert_eq!(
946 disk.properties
947 .as_ref()
948 .and_then(|p| p.unique_id.as_deref()),
949 Some("disk-uuid-abc"),
950 );
951 }
952
953 #[tokio::test]
954 async fn create_disk_sends_put_and_returns_disk() {
955 use crate::types::compute::{DiskCreateRequest, DiskCreationData, DiskProperties, DiskSku};
956 let mut mock = crate::MockClient::new();
957 mock.expect_put("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/disks/new-disk")
958 .returning_json(serde_json::json!({
959 "name": "new-disk",
960 "location": "eastus",
961 "sku": { "name": "Standard_LRS" },
962 "properties": {
963 "diskSizeGB": 4,
964 "diskState": "Unattached",
965 "provisioningState": "Updating"
966 }
967 }));
968
969 let client = AzureHttpClient::from_mock(mock);
970 let request = DiskCreateRequest {
971 location: "eastus".into(),
972 sku: Some(DiskSku {
973 name: Some("Standard_LRS".into()),
974 ..Default::default()
975 }),
976 properties: Some(DiskProperties {
977 disk_size_gb: Some(4),
978 creation_data: Some(DiskCreationData {
979 create_option: "Empty".into(),
980 ..Default::default()
981 }),
982 ..Default::default()
983 }),
984 ..Default::default()
985 };
986 let disk = client
987 .compute()
988 .create_disk("rg1", "new-disk", &request)
989 .await
990 .expect("create_disk failed");
991 assert_eq!(disk.name.as_deref(), Some("new-disk"));
992 assert_eq!(
993 disk.properties.as_ref().and_then(|p| p.disk_size_gb),
994 Some(4)
995 );
996 assert_eq!(
997 disk.properties
998 .as_ref()
999 .and_then(|p| p.provisioning_state.as_deref()),
1000 Some("Updating"),
1001 );
1002 }
1003
1004 #[tokio::test]
1005 async fn delete_disk_sends_delete() {
1006 let mut mock = crate::MockClient::new();
1007 mock.expect_delete("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/disks/old-disk")
1008 .returning_json(serde_json::json!(null));
1009
1010 let client = AzureHttpClient::from_mock(mock);
1011 client
1012 .compute()
1013 .delete_disk("rg1", "old-disk")
1014 .await
1015 .expect("delete_disk failed");
1016 }
1017
1018 #[tokio::test]
1019 async fn update_disk_sku_sends_patch_and_returns_disk() {
1020 let mut mock = crate::MockClient::new();
1021 mock.expect_patch("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/disks/my-disk")
1022 .returning_json(serde_json::json!({
1023 "name": "my-disk",
1024 "location": "eastus",
1025 "sku": { "name": "StandardSSD_LRS", "tier": "StandardSSD" },
1026 "properties": {
1027 "diskSizeGB": 128,
1028 "diskState": "Unattached",
1029 "provisioningState": "Succeeded"
1030 }
1031 }));
1032
1033 let client = AzureHttpClient::from_mock(mock);
1034 let disk = client
1035 .compute()
1036 .update_disk_sku("rg1", "my-disk", "StandardSSD_LRS")
1037 .await
1038 .expect("update_disk_sku failed");
1039 assert_eq!(disk.name.as_deref(), Some("my-disk"));
1040 assert_eq!(
1041 disk.sku.as_ref().and_then(|s| s.name.as_deref()),
1042 Some("StandardSSD_LRS")
1043 );
1044 assert_eq!(
1045 disk.properties.as_ref().and_then(|p| p.disk_size_gb),
1046 Some(128)
1047 );
1048 assert_eq!(
1049 disk.properties
1050 .as_ref()
1051 .and_then(|p| p.disk_state.as_deref()),
1052 Some("Unattached"),
1053 );
1054 assert_eq!(
1055 disk.properties
1056 .as_ref()
1057 .and_then(|p| p.provisioning_state.as_deref()),
1058 Some("Succeeded"),
1059 );
1060 }
1061
1062 #[tokio::test]
1063 async fn revoke_access_sends_post() {
1064 let mut mock = crate::MockClient::new();
1065 mock.expect_post("/subscriptions/test-subscription-id/resourceGroups/rg1/providers/Microsoft.Compute/disks/my-disk/endGetAccess")
1066 .returning_json(serde_json::json!(null));
1067
1068 let client = AzureHttpClient::from_mock(mock);
1069 client
1070 .compute()
1071 .revoke_access("rg1", "my-disk")
1072 .await
1073 .expect("revoke_access failed");
1074 }
1075}