Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cgroup/cgroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ func placeScope(parentDir, dir, cpuset string) error {
}
}
}
if err := checkSubset(cpuset, parentDir, "--cpuset-cpus"); err != nil {
if err := checkSubset(cpuset, parentDir, "cpuset_cpus placement"); err != nil {
return err
}
return writeControl(dir, cpusetName, cpuset)
Expand Down
3 changes: 1 addition & 2 deletions cmd/core/gc.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"github.com/cocoonstack/cocoon/gc"
"github.com/cocoonstack/cocoon/hypervisor"
"github.com/cocoonstack/cocoon/lock/vmlock"
"github.com/cocoonstack/cocoon/network"
"github.com/cocoonstack/cocoon/network/bridge"
"github.com/cocoonstack/cocoon/snapshot/localfile"
)
Expand Down Expand Up @@ -40,7 +39,7 @@ func NewGCOrchestrator(ctx context.Context, conf *config.Config, snapOpts ...loc
}
gc.Register(o, hypervisor.CgroupGCModule(conf.CgroupParentDir()))
netProvider.RegisterGC(o)
gc.Register(o, bridge.GCModule(network.BridgeTAPPrefix(conf.NetScope)))
gc.Register(o, bridge.GCModule(conf.BridgeTAPPrefix()))
gc.Register(o, vmlock.GCModule(conf.RootDir))
snapBackend.RegisterGC(o)
return o, nil
Expand Down
2 changes: 1 addition & 1 deletion cmd/core/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func wireHypervisor[H networkedHypervisor](newFn func(*config.Config, metering.R
}

// pinnedElsewhere unions VM and snapshot blob pins for image GC's under-lock recheck; backends build lazily, GC-path only.
func pinnedElsewhere(conf *config.Config) func(context.Context) (map[string]struct{}, error) {
func pinnedElsewhere(conf *config.Config) imagebackend.PinRecheck {
type pinner interface {
PinnedBlobIDs(context.Context) (map[string]struct{}, error)
}
Expand Down
3 changes: 1 addition & 2 deletions cmd/core/metastore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import (
"github.com/cocoonstack/cocoon/utils"
)

// A failed engine open must leave the interface nil so CloseMetaStore's nil
// check holds — a typed-nil store would panic at command teardown.
// A failed engine open must leave the interface nil so CloseMetaStore's nil check holds — a typed-nil store would panic at command teardown.
func TestMetaStoreOpenErrorThenCloseNoPanic(t *testing.T) {
resetMetaStore()
conf := testConf(t)
Expand Down
2 changes: 1 addition & 1 deletion cmd/core/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (n *NetProviders) Quiesce(ctx context.Context, vm *types.VM) error {

// A partial CNI failure leaves the tombstone for retry or GC to resume.
func (n *NetProviders) Cleanup(ctx context.Context, vmID string) error {
bridgenet.CleanupTAPs(network.BridgeTAPPrefix(n.conf.NetScope), []string{vmID})
bridgenet.CleanupTAPs(n.conf.BridgeTAPPrefix(), []string{vmID})
p, err := n.cniOnly()
if err != nil {
// Lazy CNI; OK to skip for bridge-only setups.
Expand Down
3 changes: 1 addition & 2 deletions cmd/core/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func EnsureSnapshotNameFree(ctx context.Context, snapBackend snapshot.Snapshot,
if name == "" {
return nil
}
// The name index, not Inspect: a save killed mid-flight leaves a pending record still holding the name, which Inspect reports as not-found. Passing that preflight means the whole capture is written before the insert rejects it.
// The name index, not Inspect: a killed save leaves a pending record still holding the name that Inspect reports as not-found, and passing that preflight wastes the whole capture before the insert rejects it.
if nh, ok := snapBackend.(snapshot.NameHolder); ok {
id, held, err := nh.NameOwner(ctx, name)
if err != nil {
Expand Down Expand Up @@ -311,7 +311,6 @@ func digestPullRef(image, digest, imageType string) string {
if digest == "" || imageType != types.ImageTypeOCI {
return image
}
// OCI: convert "registry/repo:tag" → "registry/repo@sha256:..."
ref, err := name.ParseReference(image)
if err != nil {
return image
Expand Down
4 changes: 1 addition & 3 deletions cmd/core/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,7 @@ func TestPersistSnapshotDirCleansCaptureOnDirectError(t *testing.T) {
}
}

// directErrSnap is a DirectCreator whose CreateFromDir always fails; the
// embedded interface panics on any other call, so the test proves the failure
// path alone.
// directErrSnap is a DirectCreator whose CreateFromDir always fails; the embedded interface panics on any other call, so the test proves the failure path alone.
type directErrSnap struct {
snapshot.Snapshot
}
Expand Down
14 changes: 2 additions & 12 deletions cmd/core/vmconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ func VMConfigFromFlags(cmd *cobra.Command, image string) (*types.VMConfig, error
mergeable, _ := cmd.Flags().GetBool("mergeable")
dataDiskRaw, _ := cmd.Flags().GetStringArray("data-disk")

if mergeable && (hugePages || sharedMemory) {
return nil, fmt.Errorf("--mergeable needs plain private memory; drop --hugepages/--shared-memory")
}

vmName = cmp.Or(vmName, sanitizeVMName(image))

memBytes, err := units.RAMInBytes(memStr)
Expand Down Expand Up @@ -212,10 +208,7 @@ func sanitizeVMName(image string) string {
n := strings.ReplaceAll(image, "/", "-")
n = strings.ReplaceAll(n, ":", "-")
n = "cocoon-" + n
if len(n) > 63 {
n = n[:63]
}
return n
return n[:min(len(n), 63)]
}

repo := strings.TrimPrefix(ref.Context().RepositoryStr(), "library/")
Expand All @@ -226,10 +219,7 @@ func sanitizeVMName(image string) string {
n += "-" + tag.TagStr()
}

if len(n) > 63 {
n = n[:63]
}
return n
return n[:min(len(n), 63)]
}

// parseDataDiskFlags parses --data-disk values, normalizes defaults, and returns the spec list ready for hypervisor.PrepareDataDisks.
Expand Down
12 changes: 4 additions & 8 deletions cmd/meta/convert/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,7 @@ func TestDistinctGenerationsRoundTrip(t *testing.T) {
t.Fatalf("convert back to json: %v", err)
}

// Corrupt the new main: the served generation must be the imported one,
// never a stranded pre-conversion .prev (§9).
// Corrupt the new main: the served generation must be the imported one, never a stranded pre-conversion .prev (§9).
main, err := os.ReadFile(spec.JSON[0].FilePath)
if err != nil {
t.Fatal(err)
Expand All @@ -313,8 +312,7 @@ func TestDistinctGenerationsRoundTrip(t *testing.T) {
}
}

// TestUncheckpointedWALReverseConversion: a commit stranded in the WAL by a
// killed process must survive the checkpoint-then-aside reverse conversion (§9).
// TestUncheckpointedWALReverseConversion: a commit stranded in the WAL by a killed process must survive the checkpoint-then-aside reverse conversion (§9).
func TestUncheckpointedWALReverseConversion(t *testing.T) {
if testing.Short() {
t.Skip("multi-process gate skipped in -short")
Expand Down Expand Up @@ -364,8 +362,7 @@ func TestUncheckpointedWALReverseConversion(t *testing.T) {
}
}

// TestWALWriterWorker commits one durable record, acks, and hangs until killed
// so the WAL is never checkpointed by a clean close.
// TestWALWriterWorker commits one durable record, acks, and hangs until killed so the WAL is never checkpointed by a clean close.
func TestWALWriterWorker(t *testing.T) {
db := os.Getenv("META_MP_DB")
if db == "" {
Expand All @@ -387,8 +384,7 @@ func TestWALWriterWorker(t *testing.T) {
select {} //nolint:staticcheck // hang until SIGKILL keeps the WAL un-checkpointed
}

// TestConvertSkipsNeverWrittenNamespace: a namespace whose lock dir was
// never created (subsystem never ran) must not fail the quiesce probe.
// TestConvertSkipsNeverWrittenNamespace: a namespace whose lock dir was never created (subsystem never ran) must not fail the quiesce probe.
func TestConvertSkipsNeverWrittenNamespace(t *testing.T) {
spec := testSpec(t, "vms")
seedJSON(t, spec, "vms")
Expand Down
1 change: 0 additions & 1 deletion cmd/vm/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ func resolveAttacher[A any](h Handler, cmd *cobra.Command, args []string, op str
return ctx, conf, hyper, a, nil
}

// classifyAttachErr surfaces ErrNotRunning more clearly than the generic wrap.
func classifyAttachErr(err error) error {
if errors.Is(err, hypervisor.ErrNotRunning) {
return fmt.Errorf("vm is not running: %w", err)
Expand Down
11 changes: 4 additions & 7 deletions cmd/vm/debug.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package vm

import (
"cmp"
"fmt"
"os"
"slices"
Expand Down Expand Up @@ -147,7 +148,7 @@ func buildCHDebugSpec(cmd *cobra.Command, conf *config.Config, storageConfigs []
case balloon == 0:
balloon = int(size >> 20) //nolint:mnd
}
allowed := cgroup.EffectiveCPUs(vmCfg.CPUSetCPUs, conf.CgroupCPUs)
allowed := cgroup.EffectiveCPUs(vmCfg.CPUSetCPUs, conf.CgroupCPUFence())
return chDebugSpec{
Configs: storageConfigs,
Boot: boot,
Expand All @@ -166,9 +167,7 @@ func printCHDebug(s chDebugSpec) {
noDirectIO := s.VMCfg.NoDirectIO

if hypervisor.IsDirectBoot(s.Boot) {
if s.CowPath == "" {
s.CowPath = fmt.Sprintf("cow-%s.raw", s.VMCfg.Name)
}
s.CowPath = cmp.Or(s.CowPath, fmt.Sprintf("cow-%s.raw", s.VMCfg.Name))
debugConfigs := slices.Concat(s.Configs, []*types.StorageConfig{
{Path: s.CowPath, RO: false, Serial: hypervisor.CowSerial},
})
Expand All @@ -189,9 +188,7 @@ func printCHDebug(s chDebugSpec) {
fmt.Print(" \\\n")
fmt.Printf(" --cmdline \"%s\" \\\n", cmdline)
} else {
if s.CowPath == "" {
s.CowPath = fmt.Sprintf("cow-%s.qcow2", s.VMCfg.Name)
}
s.CowPath = cmp.Or(s.CowPath, fmt.Sprintf("cow-%s.qcow2", s.VMCfg.Name))
basePath := s.Configs[0].Path
fmt.Println("# Prepare COW overlay")
fmt.Printf("qemu-img create -f qcow2 -F qcow2 -b %s %s\n", basePath, s.CowPath)
Expand Down
3 changes: 1 addition & 2 deletions cmd/vm/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ func TestReadHybridVsockReply(t *testing.T) {

// TestDialHybridVsock_ConnectHandshake drives the CH/FC hybrid vsock dialect (CONNECT <port>\n → OK <port>\n) against an in-process listener.
func TestDialHybridVsock_ConnectHandshake(t *testing.T) {
// macOS caps unix socket paths at ~104 bytes, so t.TempDir() (long
// /var/folders/... path) can overflow. Use os.CreateTemp + immediate unlink.
// macOS caps unix socket paths at ~104 bytes and t.TempDir() can overflow it, so use os.CreateTemp + immediate unlink.
f, err := os.CreateTemp("", "vsock-*.uds")
if err != nil {
t.Fatalf("create temp: %v", err)
Expand Down
4 changes: 3 additions & 1 deletion cmd/vm/logstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package vm
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"

"github.com/cocoonstack/cocoon/utils"
Expand All @@ -13,7 +15,7 @@ import (
func streamLog(ctx context.Context, path string, follow bool, tail int) error {
f, err := os.Open(path) //nolint:gosec
if err != nil {
if os.IsNotExist(err) {
if errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("open log %s: VM may not have been started yet", path)
}
return fmt.Errorf("open log: %w", err)
Expand Down
5 changes: 2 additions & 3 deletions cmd/vm/run.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package vm

import (
"cmp"
"context"
"fmt"
"time"
Expand Down Expand Up @@ -323,9 +324,7 @@ func (h Handler) prepareClone(ctx context.Context, cmd *cobra.Command, conf *con
return cloneSetup{}, err
}
vmID := utils.GenerateID()
if vmCfg.Name == "" {
vmCfg.Name = "cocoon-clone-" + network.VMIDPrefix(vmID)
}
vmCfg.Name = cmp.Or(vmCfg.Name, "cocoon-clone-"+network.VMIDPrefix(vmID))
if err = vmCfg.Validate(); err != nil {
return cloneSetup{}, err
}
Expand Down
5 changes: 2 additions & 3 deletions cmd/vm/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/cocoonstack/cocoon/config"
"github.com/cocoonstack/cocoon/hypervisor"
"github.com/cocoonstack/cocoon/types"
"github.com/cocoonstack/cocoon/utils"
)

type vmEvent struct {
Expand Down Expand Up @@ -54,9 +55,7 @@ func (h Handler) Status(cmd *cobra.Command, args []string) error {
ctx, conf := h.Init(cmd)

interval, _ := cmd.Flags().GetInt("interval")
if interval <= 0 {
interval = 5 //nolint:mnd
}
interval = utils.OrDefault(interval, 5) //nolint:mnd
eventMode, _ := cmd.Flags().GetBool("event")
watchMode, _ := cmd.Flags().GetBool("watch")
if eventMode && watchMode {
Expand Down
12 changes: 8 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ type Config struct {
PoolSize int `json:"pool_size" mapstructure:"pool_size"`
// PullConns: concurrent HTTP Range connections per cloud-image download; <=0 = 8.
PullConns int `json:"pull_conns" mapstructure:"pull_conns"`
// MetaBackend selects the metadata engine: "json" or "sqlite"; empty
// auto-resolves (an existing store binds its engine, fresh roots get sqlite).
// MetaBackend selects the metadata engine: "json" or "sqlite"; empty auto-resolves (an existing store binds its engine, fresh roots get sqlite).
MetaBackend string `json:"meta_backend,omitempty" mapstructure:"meta_backend"`
// CNIConfDir: CNI plugin configuration dir. Default: /etc/cni/net.d.
CNIConfDir string `json:"cni_conf_dir" mapstructure:"cni_conf_dir"`
Expand Down Expand Up @@ -92,6 +91,12 @@ func (c *Config) EffectivePullConns() int {
return min(utils.OrDefault(c.PullConns, defaultPullConns), maxPullConns)
}

// BridgeTAPPrefix returns this installation's host-side bridge TAP name prefix.
func (c *Config) BridgeTAPPrefix() string { return network.BridgeTAPPrefix(c.NetScope) }

// NetnsPrefix returns this installation's per-VM CNI netns name prefix.
func (c *Config) NetnsPrefix() string { return network.NetnsPrefix(c.NetScope) }

// CgroupParentDir returns the absolute cgroup v2 directory holding per-VM CPU scopes.
func (c *Config) CgroupParentDir() string {
return filepath.Join(cgroup.Root, cmp.Or(c.CgroupParent, cgroup.DefaultParent))
Expand All @@ -100,8 +105,7 @@ func (c *Config) CgroupParentDir() string {
// CgroupCPUFence returns the configured fence cpu list (empty = all cores).
func (c *Config) CgroupCPUFence() string { return c.CgroupCPUs }

// Validate checks that all config fields are within acceptable ranges.
// Should be called once at startup after unmarshalling.
// Validate checks that all config fields are within acceptable ranges; call once at startup after unmarshalling.
func (c *Config) Validate() error {
if c.RootDir == "" {
return fmt.Errorf("root_dir must not be empty")
Expand Down
3 changes: 1 addition & 2 deletions daemon/reconcile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ func TestReconcileSkipsInconclusiveLiveness(t *testing.T) {
}
}

// A tombstoned record is not supervised as a live VM, but the daemon starts
// deletes of its own, so it must finish one a crash left mid-protocol.
// A tombstoned record is not supervised as a live VM, but the daemon starts deletes of its own, so it must finish one a crash left mid-protocol.
func TestReconcileResumesInterruptedDelete(t *testing.T) {
f := newFake().put(runningRec("vm1", 1))
f.tombstoned["vm1"] = struct{}{}
Expand Down
10 changes: 6 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ Lightweight MicroVM engine with dual hypervisor backends:
[Cloud Hypervisor](https://github.com/cloud-hypervisor/cloud-hypervisor)
(default) and
[Firecracker](https://github.com/firecracker-microvm/firecracker).
One hypervisor process per VM, no daemon; Docker-like CLI; snapshots that
clone into new identities in tens of milliseconds.
One hypervisor process per VM, every command standalone (a resident daemon
is optional); Docker-like CLI; snapshots that clone into new identities in
tens of milliseconds.

```
cocoon CLI ──► images: OCI (EROFS layers, direct boot) | cloudimg (qcow2, UEFI)
Expand Down Expand Up @@ -47,7 +48,7 @@ cocoon CLI ──► images: OCI (EROFS layers, direct boot) | cloudimg (qcow2,
- **Image import** — import local qcow2 or tar files (also from stdin or gzip-wrapped streams), auto-detected by magic bytes
- **UEFI boot** — CLOUDHV.fd firmware by default; direct kernel boot for OCI images (auto-detected)
- **COW overlays** — copy-on-write disks backed by shared base images (raw for OCI, qcow2 for cloud images)
- **CNI networking** — automatic NIC creation via CNI plugins, multi-NIC support, per-VM IP allocation
- **CNI networking** — automatic NIC creation via CNI plugins, multi-NIC support, per-VM IP allocation; bridge mode and NIC hot-resize; `net_scope` keys host device names per installation so co-hosted cocoon roots never GC each other's guests
- **CPU isolation** — every VM runs in its own cgroup v2 scope with Guaranteed-at-N defaults (`--cpu` is a hard cap); raw weight/quota/burst knobs, an optional host-core fence (`cgroup_cpus`), and per-VM pinning (`--cpuset-cpus`); see [CPU Isolation](vm.md#cpu-isolation-cgroup-v2)
- **Multi-queue virtio-net** — TAP devices created with per-vCPU queue pairs; configurable ring depth (`--queue-size`, default 512); TSO/UFO/csum offload enabled by default
- **TC redirect I/O path** — veth ↔ TAP wired via ingress qdisc + mirred redirect (no bridge in the data path)
Expand All @@ -56,6 +57,7 @@ cocoon CLI ──► images: OCI (EROFS layers, direct boot) | cloudimg (qcow2,
- **User data disks** — `--data-disk` attaches additional virtio-blk disks per VM, with optional ext4 mkfs at create time, cloud-init `mounts:` auto-mount on cloudimg+CH (via `/dev/disk/by-id/virtio-<name>`), per-disk DirectIO override, and 1:1 inheritance through snapshot/clone/restore; `vm clone --data-disk` adds fresh disks to a clone and `vm disk attach/detach` hot-plugs existing raw files on a running VM (both CH only)
- **Copy-on-write clone restore** — Cloud Hypervisor clones of plain private-anon snapshots default to `mmap` memory restore: no eager copy, page cache shared across sibling clones; hugepages/shared snapshots fall back to eager copy with a warning
- **Hugepages** — opt-in via `vm create --hugepages` (Cloud Hypervisor only); backs VM memory with hugetlbfs at the cost of the mmap restore fast path for that VM's snapshots (never supported on Firecracker, whose snapshots cannot restore from hugetlbfs)
- **Mergeable memory** — opt-in `--mergeable` (Cloud Hypervisor only) madvises guest memory `MADV_MERGEABLE` so host KSM can dedup identical pages across VMs; excludes `--hugepages`/`--shared-memory`
- **Memory balloon** — 25% of memory returned via virtio-balloon (deflate-on-OOM, free-page reporting) when memory >= 256 MiB
- **Graceful shutdown** — ACPI power-button for UEFI VMs with configurable timeout, fallback to SIGTERM → SIGKILL
- **Interactive console** — `cocoon vm console` with bidirectional PTY relay, SSH-style escape sequences (`~.` disconnect, `~?` help), configurable escape character, SIGWINCH propagation
Expand All @@ -67,7 +69,7 @@ cocoon CLI ──► images: OCI (EROFS layers, direct boot) | cloudimg (qcow2,
- **Structured logging** — configurable log level (`--log-level`), log rotation (max size / age / backups)
- **Debug command** — `cocoon vm debug` generates a copy-pasteable `cloud-hypervisor` command for manual debugging
- **Firecracker backend** — `--fc` flag selects Firecracker for OCI images: ~125ms boot, <5 MiB overhead, minimal attack surface (no UEFI, no qcow2, no Windows)
- **Zero-daemon architecture** — one hypervisor process per VM, no long-running daemon
- **Daemon-optional architecture** — one hypervisor process per VM, every command standalone; `cocoon daemon` optionally adopts running VMs to converge crashes as they happen
- **Garbage collection** — modular lock-safe GC with cross-module snapshot resolution; protects blobs referenced by running VMs and snapshots
- **Doctor script** — pre-flight environment check and one-command dependency installation

Expand Down
11 changes: 3 additions & 8 deletions extend/disk/disk.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// Package disk is the runtime attach interface for extra virtio-blk disks
// backed by existing raw files. Attach is runtime-only — snapshot/hibernate
// is refused while one is attached; detach never deletes the backing file.
// Package disk is the runtime attach interface for extra virtio-blk disks backed by existing raw files; attach is runtime-only (snapshot/hibernate refuse while one is attached) and detach never deletes the backing file.
package disk

import (
Expand All @@ -18,9 +16,7 @@ const diskIDPrefix = "cocoon-disk-"
// ErrUnsupportedBackend signals the backend cannot hot-plug virtio-blk disks (e.g. Firecracker).
var ErrUnsupportedBackend = errors.New("backend does not support disk attach")

// Spec is one attach request: an existing raw disk file. DirectIO nil means
// the create-path default (O_DIRECT unless writable disks have it disabled);
// tmpfs-backed files need an explicit off.
// Spec is one attach request for an existing raw disk file; DirectIO nil means the create-path default (O_DIRECT unless writable disks have it disabled), tmpfs-backed files need an explicit off.
type Spec struct {
Path string
Name string
Expand Down Expand Up @@ -69,8 +65,7 @@ func DeriveID(name string) string {
return diskIDPrefix + name
}

// NameFromID reverses DeriveID; empty when id is not a hot-added disk
// (foreign same-prefix ids with an illegal name suffix are not ours).
// NameFromID reverses DeriveID; empty when id is not a hot-added disk (foreign same-prefix ids with an illegal name suffix are not ours).
func NameFromID(id string) string {
name, ok := strings.CutPrefix(id, diskIDPrefix)
if ok && types.ValidDataDiskName(name) {
Expand Down
Loading