Skip to content

Commit 346a14c

Browse files
committed
major changes for windows support
1 parent f974f4e commit 346a14c

File tree

16 files changed

+412
-35
lines changed

16 files changed

+412
-35
lines changed

.github/workflows/build-os-images.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [master]
66
paths:
77
- "os-image/**"
8+
- "!os-image/windows/**"
89
workflow_dispatch:
910
inputs:
1011
no-cache:

KNOWN_ISSUES.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,23 @@ This applies to **all CNI plugins** where the upstream network provides DHCP (br
7070
```
7171

7272
Cocoon detects when CNI returns no IP allocation and automatically configures the guest for DHCP — cloudimg VMs get `DHCP=ipv4` in their Netplan config, and OCI VMs get DHCP systemd-networkd units generated by the initramfs.
73+
74+
## Windows VM requires Cloud Hypervisor v50.2
75+
76+
Cloud Hypervisor v51.x has a regression ([#7849](https://github.com/cloud-hypervisor/cloud-hypervisor/issues/7849)) that causes Windows to BSOD (`DRIVER_IRQL_NOT_LESS_OR_EQUAL` in `viostor.sys`) when DISCARD/WRITE_ZEROES features are advertised with default-zero config values, violating virtio spec v1.2. The fix (PR #7852) is merged but not yet included in any release.
77+
78+
**Recommendation**: use Cloud Hypervisor **v50.2** for Windows VMs.
79+
80+
## Windows VM requires virtio-win 0.1.240
81+
82+
virtio-win 0.1.271+ network drivers are incompatible with Cloud Hypervisor due to incomplete virtio-net control queue implementation ([#7925](https://github.com/cloud-hypervisor/cloud-hypervisor/issues/7925)). CH only handles `CTRL_MQ` and `CTRL_GUEST_OFFLOADS`; all other commands (`CTRL_RX`, `CTRL_MAC`, `CTRL_VLAN`, `CTRL_ANNOUNCE`) return `VIRTIO_NET_ERR`.
83+
84+
| Version | Behavior on VIRTIO_NET_ERR |
85+
|----------|----------------------------------------------------------|
86+
| 0.1.240 | Tolerates error, continues working |
87+
| 0.1.271 | May silently fail, NIC unusable |
88+
| 0.1.285+ | Fail-fast: NdisMRemoveMiniport(), Problem Code 43 |
89+
90+
0.1.285 introduced commit `50e7db9` ("indicate driver error on unexpected CX behavior") with zero-tolerance on control queue errors. Root cause is a CH bug — correct fix is to return `VIRTIO_NET_OK` for unsupported commands instead of `VIRTIO_NET_ERR`. No upstream PR exists yet.
91+
92+
**Recommendation**: use virtio-win **0.1.240** for Windows VMs on Cloud Hypervisor.

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Lightweight MicroVM engine built on [Cloud Hypervisor](https://github.com/cloud-
2929

3030
- Linux with KVM (x86_64 or aarch64)
3131
- Root access (sudo)
32-
- [Cloud Hypervisor](https://github.com/cloud-hypervisor/cloud-hypervisor) v51.0+
32+
- [Cloud Hypervisor](https://github.com/cloud-hypervisor/cloud-hypervisor) v51.0+ (v50.2 for Windows VMs — see [KNOWN_ISSUES.md](KNOWN_ISSUES.md))
3333
- `qemu-img` (from qemu-utils, for cloud images)
3434
- UEFI firmware (`CLOUDHV.fd`, for cloud images)
3535
- CNI plugins (`bridge`, `host-local`, `loopback`)
@@ -168,6 +168,7 @@ Applies to `cocoon vm create`, `cocoon vm run`, and `cocoon vm debug`:
168168
| `--storage` | `10G` | COW disk size (e.g., 10G, 20G) |
169169
| `--nics` | `1` | Number of network interfaces (0 = no network) |
170170
| `--network` | empty (default) | CNI conflist name (empty = first conflist) |
171+
| `--windows` | `false` | Windows guest (UEFI boot, kvm_hyperv=on, no cidata) |
171172

172173
### Clone Flags
173174

@@ -276,6 +277,35 @@ Cloudimg VMs receive a NoCloud cidata disk (FAT12 with `CIDATA` volume label) co
276277

277278
The cidata disk is **automatically excluded on subsequent boots** — after the first successful start, the VM record is marked as `first_booted` and the cidata disk is no longer attached, preventing cloud-init from re-running.
278279

280+
## Windows Support
281+
282+
Cocoon supports Windows guests via the `--windows` flag:
283+
284+
```bash
285+
cocoon vm run --windows --name win11 --cpu 2 --memory 4G --storage 40G <cloudimg-url>
286+
```
287+
288+
The `--windows` flag:
289+
- Forces UEFI firmware boot (cloudimg path)
290+
- Enables Hyper-V enlightenments (`kvm_hyperv=on`)
291+
- Skips cloud-init cidata disk generation (Windows does not use cloud-init)
292+
293+
### Requirements
294+
295+
- Cloud Hypervisor **v50.2** (v51.x has a Windows regression — see [KNOWN_ISSUES.md](KNOWN_ISSUES.md))
296+
- virtio-win **0.1.240** drivers pre-installed in the image (see [KNOWN_ISSUES.md](KNOWN_ISSUES.md))
297+
298+
### Image Preparation
299+
300+
Windows images must be prepared manually — Microsoft licensing prohibits automated distribution of Windows disk images. See [`os-image/windows/`](os-image/windows/) for the complete build guide and `autounattend.xml` for unattended installation.
301+
302+
### Post-Clone Networking
303+
304+
- **DHCP networks**: no action needed, Windows DHCP client auto-configures
305+
- **Static IP**: configure via SAC serial console (`cocoon vm console`)
306+
307+
For more details, see the [Cloud Hypervisor Windows documentation](https://github.com/cloud-hypervisor/cloud-hypervisor/blob/main/docs/windows.md).
308+
279309
## VM Lifecycle
280310

281311
| State | Description |

cmd/core/helpers.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ func VMConfigFromFlags(cmd *cobra.Command, image string) (*types.VMConfig, error
160160
memStr, _ := cmd.Flags().GetString("memory")
161161
storStr, _ := cmd.Flags().GetString("storage")
162162
network, _ := cmd.Flags().GetString("network")
163+
windows, _ := cmd.Flags().GetBool("windows")
163164

164165
if vmName == "" {
165166
vmName = sanitizeVMName(image)
@@ -181,6 +182,7 @@ func VMConfigFromFlags(cmd *cobra.Command, image string) (*types.VMConfig, error
181182
Storage: storBytes,
182183
Image: image,
183184
Network: network,
185+
Windows: windows,
184186
}
185187
if err := cfg.Validate(); err != nil {
186188
return nil, err
@@ -241,6 +243,7 @@ func CloneVMConfigFromFlags(cmd *cobra.Command, snapCfg *types.SnapshotConfig) (
241243
Storage: storBytes,
242244
Image: snapCfg.Image,
243245
Network: network,
246+
Windows: snapCfg.Windows,
244247
}
245248
if err := cfg.Validate(); err != nil {
246249
return nil, err

cmd/vm/commands.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ func addVMFlags(cmd *cobra.Command) {
142142
cmd.Flags().String("storage", "10G", "COW disk size") //nolint:mnd
143143
cmd.Flags().Int("nics", 1, "number of network interfaces (0 = no network); multiple NICs with auto IP config only works for cloudimg; OCI images auto-configure only the last NIC, others require manual setup inside the guest")
144144
cmd.Flags().String("network", "", "CNI conflist name (empty = default)")
145+
cmd.Flags().Bool("windows", false, "Windows guest (UEFI boot, kvm_hyperv=on, no cidata)")
145146
}
146147

147148
func addCloneFlags(cmd *cobra.Command) {

cmd/vm/handler.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -484,9 +484,9 @@ func (h Handler) Debug(cmd *cobra.Command, args []string) error {
484484
}
485485

486486
if boot.KernelPath != "" {
487-
printRunOCI(storageConfigs, boot, vmCfg.Name, vmCfg.Image, cowPath, chBin, vmCfg.CPU, maxCPU, memoryMB, balloon, cowSizeGB)
487+
printRunOCI(storageConfigs, boot, vmCfg.Name, vmCfg.Image, cowPath, chBin, vmCfg.CPU, maxCPU, memoryMB, balloon, cowSizeGB, vmCfg.Windows)
488488
} else {
489-
printRunCloudimg(storageConfigs, boot, vmCfg.Name, vmCfg.Image, cowPath, chBin, vmCfg.CPU, maxCPU, memoryMB, balloon, cowSizeGB)
489+
printRunCloudimg(storageConfigs, boot, vmCfg.Name, vmCfg.Image, cowPath, chBin, vmCfg.CPU, maxCPU, memoryMB, balloon, cowSizeGB, vmCfg.Windows)
490490
}
491491
return nil
492492
}
@@ -524,6 +524,9 @@ func (h Handler) createVM(cmd *cobra.Command, image string) (context.Context, *t
524524
if err != nil {
525525
return nil, nil, nil, err
526526
}
527+
if vmCfg.Windows && bootCfg.KernelPath != "" {
528+
return nil, nil, nil, fmt.Errorf("--windows requires cloudimg (UEFI boot), got OCI direct boot image")
529+
}
527530
cmdcore.EnsureFirmwarePath(conf, bootCfg)
528531

529532
vmID, err := utils.GenerateID()
@@ -589,6 +592,16 @@ func batchVMCmd(ctx context.Context, name, pastTense string, fn func(context.Con
589592
// printPostCloneHints outputs commands the user should run inside the guest
590593
// after a clone to reconfigure network and release balloon memory.
591594
func printPostCloneHints(vm *types.VM, networkConfigs []*types.NetworkConfig) {
595+
if vm.Config.Windows {
596+
fmt.Println()
597+
fmt.Println("Windows clone: NICs hot-swapped with new MAC addresses.")
598+
fmt.Println(" DHCP networks: no action needed.")
599+
fmt.Println(" Static IP: configure via SAC serial console (cocoon vm console):")
600+
fmt.Println(" https://github.com/cloud-hypervisor/cloud-hypervisor/blob/main/docs/windows.md")
601+
fmt.Println()
602+
return
603+
}
604+
592605
isCloudimg := slices.ContainsFunc(vm.StorageConfigs, func(sc *types.StorageConfig) bool {
593606
return strings.HasSuffix(sc.Path, ".qcow2")
594607
})
@@ -705,7 +718,7 @@ func printBashArray(name string, nics []nicHint, field func(nicHint) string) {
705718
fmt.Println(")")
706719
}
707720

708-
func printRunOCI(configs []*types.StorageConfig, boot *types.BootConfig, vmName, image, cowPath, chBin string, cpu, maxCPU, memory, balloon, cowSize int) {
721+
func printRunOCI(configs []*types.StorageConfig, boot *types.BootConfig, vmName, image, cowPath, chBin string, cpu, maxCPU, memory, balloon, cowSize int, windows bool) {
709722
if cowPath == "" {
710723
cowPath = fmt.Sprintf("cow-%s.raw", vmName)
711724
}
@@ -735,10 +748,10 @@ func printRunOCI(configs []*types.StorageConfig, boot *types.BootConfig, vmName,
735748
}
736749
fmt.Printf(" \\\n")
737750
fmt.Printf(" --cmdline \"%s\" \\\n", cmdline)
738-
printCommonCHArgs(cpu, maxCPU, memory, balloon)
751+
printCommonCHArgs(cpu, maxCPU, memory, balloon, windows)
739752
}
740753

741-
func printRunCloudimg(configs []*types.StorageConfig, boot *types.BootConfig, vmName, image, cowPath, chBin string, cpu, maxCPU, memory, balloon, cowSize int) {
754+
func printRunCloudimg(configs []*types.StorageConfig, boot *types.BootConfig, vmName, image, cowPath, chBin string, cpu, maxCPU, memory, balloon, cowSize int, windows bool) {
742755
if cowPath == "" {
743756
cowPath = fmt.Sprintf("cow-%s.qcow2", vmName)
744757
}
@@ -758,7 +771,7 @@ func printRunCloudimg(configs []*types.StorageConfig, boot *types.BootConfig, vm
758771
fmt.Printf(" --disk \\\n")
759772
diskArgs := cloudhypervisor.DebugDiskCLIArgs([]*types.StorageConfig{{Path: cowPath, RO: false}}, cpu)
760773
fmt.Printf(" \"%s\" \\\n", diskArgs[0])
761-
printCommonCHArgs(cpu, maxCPU, memory, balloon)
774+
printCommonCHArgs(cpu, maxCPU, memory, balloon, windows)
762775
}
763776

764777
// vmIPs extracts a comma-separated IP string from a VM's NetworkConfigs.
@@ -778,8 +791,12 @@ func vmIPs(vm *types.VM) string {
778791
// printCommonCHArgs outputs CH args for manual debugging.
779792
// --serial tty outputs to the current terminal for interactive debugging,
780793
// which intentionally differs from the automated path (Console: Pty / Serial: Socket).
781-
func printCommonCHArgs(cpu, maxCPU, memory, balloon int) {
782-
fmt.Printf(" --cpus boot=%d,max=%d \\\n", cpu, maxCPU)
794+
func printCommonCHArgs(cpu, maxCPU, memory, balloon int, windows bool) {
795+
cpuExtra := ""
796+
if windows {
797+
cpuExtra = ",kvm_hyperv=on"
798+
}
799+
fmt.Printf(" --cpus boot=%d,max=%d%s \\\n", cpu, maxCPU, cpuExtra)
783800
fmt.Printf(" --memory size=%dM \\\n", memory)
784801
fmt.Printf(" --rng src=/dev/urandom \\\n")
785802
fmt.Printf(" --balloon size=%dM,deflate_on_oom=on,free_page_reporting=on \\\n", balloon)

hypervisor/cloudhypervisor/api.go

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ type chPayload struct {
4040
}
4141

4242
type chCPUs struct {
43-
BootVCPUs int `json:"boot_vcpus"`
44-
MaxVCPUs int `json:"max_vcpus"`
43+
BootVCPUs int `json:"boot_vcpus"`
44+
MaxVCPUs int `json:"max_vcpus"`
45+
KVMHyperV bool `json:"kvm_hyperv,omitempty"`
4546
}
4647

4748
type chMemory struct {
@@ -50,17 +51,22 @@ type chMemory struct {
5051
}
5152

5253
type chDisk struct {
53-
ID string `json:"id,omitempty"`
54-
Path string `json:"path"`
55-
ReadOnly bool `json:"readonly,omitempty"`
56-
DirectIO bool `json:"direct,omitempty"`
57-
Sparse bool `json:"sparse,omitempty"`
58-
ImageType string `json:"image_type,omitempty"`
59-
BackingFiles bool `json:"backing_files,omitempty"`
60-
NumQueues int `json:"num_queues,omitempty"`
61-
QueueSize int `json:"queue_size,omitempty"`
62-
QueueAffinity string `json:"queue_affinity,omitempty"`
63-
Serial string `json:"serial,omitempty"`
54+
ID string `json:"id,omitempty"`
55+
Path string `json:"path"`
56+
ReadOnly bool `json:"readonly,omitempty"`
57+
DirectIO bool `json:"direct,omitempty"`
58+
Sparse bool `json:"sparse,omitempty"`
59+
ImageType string `json:"image_type,omitempty"`
60+
BackingFiles bool `json:"backing_files,omitempty"`
61+
NumQueues int `json:"num_queues,omitempty"`
62+
QueueSize int `json:"queue_size,omitempty"`
63+
QueueAffinity []chQueueAffinity `json:"queue_affinity,omitempty"`
64+
Serial string `json:"serial,omitempty"`
65+
}
66+
67+
type chQueueAffinity struct {
68+
QueueIndex int `json:"queue_index"`
69+
HostCPUs []int `json:"host_cpus"`
6470
}
6571

6672
type chBalloon struct {

hypervisor/cloudhypervisor/args.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"path/filepath"
77
"runtime"
8+
"strconv"
89
"strings"
910

1011
"github.com/projecteru2/core/log"
@@ -38,7 +39,7 @@ func buildVMConfig(ctx context.Context, rec *hypervisor.VMRecord, consoleSockPat
3839
}
3940

4041
cfg := &chVMConfig{
41-
CPUs: chCPUs{BootVCPUs: cpu, MaxVCPUs: maxVCPUs},
42+
CPUs: chCPUs{BootVCPUs: cpu, MaxVCPUs: maxVCPUs, KVMHyperV: rec.Config.Windows},
4243
Memory: chMemory{Size: mem, HugePages: utils.DetectHugePages()},
4344
RNG: chRNG{Src: "/dev/urandom"},
4445
Watchdog: true,
@@ -132,11 +133,10 @@ func storageConfigToDisk(storageConfig *types.StorageConfig, cpuCount int) chDis
132133
// cross-core cache bouncing on writable disks. Skip for readonly
133134
// disks where IO is low and fully served by page cache.
134135
if cpuCount > 1 && !storageConfig.RO {
135-
parts := make([]string, cpuCount)
136-
for i := range parts {
137-
parts[i] = fmt.Sprintf("%d@[%d]", i, i)
136+
d.QueueAffinity = make([]chQueueAffinity, cpuCount)
137+
for i := range d.QueueAffinity {
138+
d.QueueAffinity[i] = chQueueAffinity{QueueIndex: i, HostCPUs: []int{i}}
138139
}
139-
d.QueueAffinity = "[" + strings.Join(parts, ",") + "]"
140140
}
141141
return d
142142
}
@@ -157,7 +157,11 @@ func DebugDiskCLIArgs(storageConfigs []*types.StorageConfig, cpuCount int) []str
157157
func buildCLIArgs(cfg *chVMConfig, socketPath string) []string {
158158
args := []string{"--api-socket", socketPath}
159159

160-
args = append(args, "--cpus", fmt.Sprintf("boot=%d,max=%d", cfg.CPUs.BootVCPUs, cfg.CPUs.MaxVCPUs))
160+
var cpuKV kvBuilder
161+
cpuKV.add(fmt.Sprintf("boot=%d", cfg.CPUs.BootVCPUs))
162+
cpuKV.add(fmt.Sprintf("max=%d", cfg.CPUs.MaxVCPUs))
163+
cpuKV.addIf(cfg.CPUs.KVMHyperV, "kvm_hyperv=on")
164+
args = append(args, "--cpus", cpuKV.String())
161165

162166
mem := fmt.Sprintf("size=%d", cfg.Memory.Size)
163167
if cfg.Memory.HugePages {
@@ -235,7 +239,9 @@ func diskToCLIArg(d chDisk) string {
235239
b.addIf(d.BackingFiles, "backing_files=on")
236240
b.addIf(d.NumQueues > 0, fmt.Sprintf("num_queues=%d", d.NumQueues))
237241
b.addIf(d.QueueSize > 0, fmt.Sprintf("queue_size=%d", d.QueueSize))
238-
b.addIf(d.QueueAffinity != "", "queue_affinity="+d.QueueAffinity)
242+
if len(d.QueueAffinity) > 0 {
243+
b.add("queue_affinity=" + queueAffinityToCLI(d.QueueAffinity))
244+
}
239245
b.addIf(d.Serial != "", "serial="+d.Serial)
240246
return b.String()
241247
}
@@ -273,6 +279,20 @@ func runtimeFiletoCLIArg(c *chRuntimeFile) string {
273279
}
274280
}
275281

282+
// queueAffinityToCLI converts structured queue affinity to CH CLI format.
283+
// e.g. []chQueueAffinity{{0,[0]},{1,[1]}} → "[0@[0],1@[1]]"
284+
func queueAffinityToCLI(qa []chQueueAffinity) string {
285+
parts := make([]string, len(qa))
286+
for i, a := range qa {
287+
cpus := make([]string, len(a.HostCPUs))
288+
for j, c := range a.HostCPUs {
289+
cpus[j] = strconv.Itoa(c)
290+
}
291+
parts[i] = fmt.Sprintf("%d@[%s]", a.QueueIndex, strings.Join(cpus, ","))
292+
}
293+
return "[" + strings.Join(parts, ",") + "]"
294+
}
295+
276296
// isCidataDisk reports whether a storage config is the cloud-init cidata disk.
277297
func isCidataDisk(sc *types.StorageConfig) bool {
278298
return filepath.Base(sc.Path) == cidataFile

hypervisor/cloudhypervisor/clone.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ func (ch *CloudHypervisor) cloneAfterExtract(ctx context.Context, vmID string, v
9494

9595
// vm.restore requires config/state device tree equality.
9696
// If snapshot had no cidata disk, patch only snapshot disks and hotplug cidata later.
97-
patchStorageConfigs := restorePatchStorageConfigs(storageConfigs, directBoot, hadCidataInSnapshot)
97+
// Windows VMs never have cidata, so skip the trim entirely.
98+
patchStorageConfigs := restorePatchStorageConfigs(storageConfigs, directBoot, vmCfg.Windows || hadCidataInSnapshot)
9899

99100
consoleSock := consoleSockPath(runDir)
100101
if err = patchCHConfig(chConfigPath, &patchOptions{
@@ -138,7 +139,7 @@ func (ch *CloudHypervisor) cloneAfterExtract(ctx context.Context, vmID string, v
138139
return nil, fmt.Errorf("launch CH: %w", err)
139140
}
140141

141-
if err := ch.restoreAndResumeClone(ctx, pid, sockPath, runDir, directBoot, hadCidataInSnapshot, storageConfigs, networkConfigs, chCfg, vmCfg.CPU); err != nil {
142+
if err := ch.restoreAndResumeClone(ctx, pid, sockPath, runDir, directBoot, vmCfg.Windows, hadCidataInSnapshot, storageConfigs, networkConfigs, chCfg, vmCfg.CPU); err != nil {
142143
return nil, err
143144
}
144145

@@ -179,7 +180,7 @@ func (ch *CloudHypervisor) restoreAndResumeClone(
179180
ctx context.Context,
180181
pid int,
181182
sockPath, runDir string,
182-
directBoot, hadCidataInSnapshot bool,
183+
directBoot, windows, hadCidataInSnapshot bool,
183184
storageConfigs []*types.StorageConfig,
184185
networkConfigs []*types.NetworkConfig,
185186
snapshotCfg *chVMConfig,
@@ -203,7 +204,7 @@ func (ch *CloudHypervisor) restoreAndResumeClone(
203204
return fmt.Errorf("hot-swap NICs: %w", err)
204205
}
205206

206-
if !directBoot && !hadCidataInSnapshot {
207+
if !directBoot && !windows && !hadCidataInSnapshot {
207208
if len(storageConfigs) == 0 {
208209
return fmt.Errorf("vm.add-disk (cidata): missing storage config")
209210
}
@@ -219,7 +220,7 @@ func (ch *CloudHypervisor) restoreAndResumeClone(
219220
}
220221

221222
func (ch *CloudHypervisor) ensureCloneCidata(vmID string, vmCfg *types.VMConfig, networkConfigs []*types.NetworkConfig, storageConfigs []*types.StorageConfig, directBoot bool) ([]*types.StorageConfig, error) {
222-
if directBoot {
223+
if directBoot || vmCfg.Windows {
223224
return storageConfigs, nil
224225
}
225226
if err := ch.generateCidata(vmID, vmCfg, networkConfigs); err != nil {

hypervisor/cloudhypervisor/create.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ func (ch *CloudHypervisor) prepareCloudimg(ctx context.Context, vmID string, vmC
156156
}
157157
}
158158

159+
// Windows: no cloud-init cidata disk.
160+
if vmCfg.Windows {
161+
return []*types.StorageConfig{
162+
{Path: overlayPath, RO: false},
163+
}, nil
164+
}
165+
159166
// Generate cloud-init cidata disk.
160167
if err := ch.generateCidata(vmID, vmCfg, networkConfigs); err != nil {
161168
return nil, err

0 commit comments

Comments
 (0)