Skip to content
Open
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
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,60 @@ to be able to use Migratekit:
| Guest operations | Allow creation and management of VM snapshots for replication. | Virtual machines | VirtualMachine.GuestOperations.* |
| Interaction Power Off | Permit powering off the VM during migration to Migratekit. | Virtual machines | VirtualMachine.Interact.PowerOff |

### Local Disk Backup

Migratekit now supports local disk backup as an alternative to OpenStack targets. This is useful for creating local backups or for testing purposes.

#### Local Disk Features:
- **CBT Support**: Uses VMware Changed Block Tracking for incremental backups
- **Change ID Storage**: Stores change IDs in local JSON metadata files
- **Raw Disk Images**: Creates raw disk images compatible with most virtualization platforms
- **Directory Structure**: Organizes backups by VM name and disk key

#### Local Disk Usage:

For the first backup (full copy):
```bash
migratekit migrate \
--vmware-endpoint vmware.local \
--vmware-username username \
--vmware-password password \
--vmware-path /ha-datacenter/vm/migration-test \
--local-target-path /backup/vm-backups
```

For subsequent incremental backups:
```bash
migratekit migrate \
--vmware-endpoint vmware.local \
--vmware-username username \
--vmware-password password \
--vmware-path /ha-datacenter/vm/migration-test \
--local-target-path /backup/vm-backups
```

For cutover (final sync):
```bash
migratekit cutover \
--vmware-endpoint vmware.local \
--vmware-username username \
--vmware-password password \
--vmware-path /ha-datacenter/vm/migration-test \
--local-target-path /backup/vm-backups
```

#### Local Disk Output Structure:
```
/backup/vm-backups/
└── migration-test/
├── disk-2000.raw
├── disk-2000.metadata.json
├── disk-2001.raw
└── disk-2001.metadata.json
```

The metadata JSON files contain the change ID and other backup information for incremental backup tracking.

### Running Migratekit

Assuming that you already have Docker installed on your system, you can use the
Expand Down Expand Up @@ -189,6 +243,7 @@ There are a few optional flags to define the following:
Valid values for the most OpenStack installations are "linux"
and "windows"
- `--enable-qemu-guest-agent`: Sets the "hw_qemu_guest_agent" volume (image) metadata parameter to "yes".
- `--local-target-path`: Local directory path for storing VM disks (alternative to OpenStack)

## Contributing

Expand Down
166 changes: 166 additions & 0 deletions internal/target/local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package target

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

log "github.com/sirupsen/logrus"
"github.com/vexxhost/migratekit/internal/vmware"
"github.com/vmware/govmomi/object"
"github.com/vmware/govmomi/vim25/types"
)

type LocalDisk struct {
VirtualMachine *object.VirtualMachine
Disk *types.VirtualDisk
BasePath string
}

type LocalDiskMetadata struct {
ChangeID string `json:"change_id"`
VMName string `json:"vm_name"`
DiskKey int `json:"disk_key"`
Size int64 `json:"size"`
}

func NewLocalDisk(ctx context.Context, vm *object.VirtualMachine, disk *types.VirtualDisk, basePath string) (*LocalDisk, error) {
// Ensure base path exists
if err := os.MkdirAll(basePath, 0755); err != nil {
return nil, fmt.Errorf("failed to create base path %s: %w", basePath, err)
}

return &LocalDisk{
VirtualMachine: vm,
Disk: disk,
BasePath: basePath,
}, nil
}

func (t *LocalDisk) GetDisk() *types.VirtualDisk {
return t.Disk
}

func (t *LocalDisk) Connect(ctx context.Context) error {
// For local disk, we just ensure the target file exists
targetPath := t.getTargetPath()
dir := filepath.Dir(targetPath)

if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create target directory %s: %w", dir, err)
}

// Create the target file if it doesn't exist
if _, err := os.Stat(targetPath); os.IsNotExist(err) {
file, err := os.Create(targetPath)
if err != nil {
return fmt.Errorf("failed to create target file %s: %w", targetPath, err)
}
defer file.Close()

// Pre-allocate space by seeking to the end
if _, err := file.Seek(t.Disk.CapacityInBytes-1, 0); err != nil {
return fmt.Errorf("failed to seek to end of file: %w", err)
}
if _, err := file.Write([]byte{0}); err != nil {
return fmt.Errorf("failed to write to end of file: %w", err)
}

log.WithFields(log.Fields{
"file": targetPath,
"size": t.Disk.CapacityInBytes,
}).Info("Created target file")
}

return nil
}

func (t *LocalDisk) GetPath(ctx context.Context) (string, error) {
return t.getTargetPath(), nil
}

func (t *LocalDisk) Disconnect(ctx context.Context) error {
// For local disk, no special disconnect needed
return nil
}

func (t *LocalDisk) Exists(ctx context.Context) (bool, error) {
targetPath := t.getTargetPath()
if _, err := os.Stat(targetPath); os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, err
}
return true, nil
}

func (t *LocalDisk) GetCurrentChangeID(ctx context.Context) (*vmware.ChangeID, error) {
metadataPath := t.getMetadataPath()
if _, err := os.Stat(metadataPath); os.IsNotExist(err) {
return &vmware.ChangeID{}, nil
}

data, err := os.ReadFile(metadataPath)
if err != nil {
return nil, fmt.Errorf("failed to read metadata file: %w", err)
}

var metadata LocalDiskMetadata
if err := json.Unmarshal(data, &metadata); err != nil {
return nil, fmt.Errorf("failed to parse metadata: %w", err)
}

if metadata.ChangeID == "" {
return &vmware.ChangeID{}, nil
}

return vmware.ParseChangeID(metadata.ChangeID)
}

func (t *LocalDisk) WriteChangeID(ctx context.Context, changeID *vmware.ChangeID) error {
metadataPath := t.getMetadataPath()

metadata := LocalDiskMetadata{
ChangeID: changeID.Value,
VMName: t.VirtualMachine.Name(),
DiskKey: int(t.Disk.Key),
Size: t.Disk.CapacityInBytes,
}

data, err := json.MarshalIndent(metadata, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal metadata: %w", err)
}

// Ensure metadata directory exists
if err := os.MkdirAll(filepath.Dir(metadataPath), 0755); err != nil {
return fmt.Errorf("failed to create metadata directory: %w", err)
}

if err := os.WriteFile(metadataPath, data, 0644); err != nil {
return fmt.Errorf("failed to write metadata file: %w", err)
}

log.WithFields(log.Fields{
"metadata_file": metadataPath,
"change_id": changeID.Value,
}).Info("Wrote change ID to metadata file")

return nil
}

func (t *LocalDisk) getTargetPath() string {
vmName := strings.ReplaceAll(t.VirtualMachine.Name(), "/", "_")
diskKey := strconv.Itoa(int(t.Disk.Key))
return filepath.Join(t.BasePath, vmName, fmt.Sprintf("disk-%s.raw", diskKey))
}

func (t *LocalDisk) getMetadataPath() string {
vmName := strings.ReplaceAll(t.VirtualMachine.Name(), "/", "_")
diskKey := strconv.Itoa(int(t.Disk.Key))
return filepath.Join(t.BasePath, vmName, fmt.Sprintf("disk-%s.metadata.json", diskKey))
}
42 changes: 28 additions & 14 deletions internal/vmware_nbdkit/vmware_nbdkit.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,16 @@ func (s *NbdkitServers) MigrationCycle(ctx context.Context, runV2V bool) error {
}()

for index, server := range s.Servers {
t, err := target.NewOpenStack(ctx, s.VirtualMachine, server.Disk)
var t target.Target
var err error

// Check if we're using local disk target
if localTargetPath := ctx.Value("localTargetPath"); localTargetPath != nil {
t, err = target.NewLocalDisk(ctx, s.VirtualMachine, server.Disk, localTargetPath.(string))
} else {
t, err = target.NewOpenStack(ctx, s.VirtualMachine, server.Disk)
}

if err != nil {
return err
}
Expand Down Expand Up @@ -357,23 +366,28 @@ func (s *NbdkitServer) SyncToTarget(ctx context.Context, t target.Target, runV2V
}

if runV2V {
log.Info("Running virt-v2v-in-place")
// Check if we're using local disk - virt-v2v-in-place may not be needed for local backups
if _, isLocalDisk := t.(*target.LocalDisk); isLocalDisk {
log.Info("Skipping virt-v2v-in-place for local disk target")
} else {
log.Info("Running virt-v2v-in-place")

os.Setenv("LIBGUESTFS_BACKEND", "direct")
os.Setenv("LIBGUESTFS_BACKEND", "direct")

var cmd *exec.Cmd
if s.Servers.VddkConfig.Debug {
cmd = exec.Command("virt-v2v-in-place", "-v", "-x", "-i", "disk", path)
} else {
cmd = exec.Command("virt-v2v-in-place", "-i", "disk", path)
}
var cmd *exec.Cmd
if s.Servers.VddkConfig.Debug {
cmd = exec.Command("virt-v2v-in-place", "-v", "-x", "-i", "disk", path)
} else {
cmd = exec.Command("virt-v2v-in-place", "-i", "disk", path)
}

cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err := cmd.Run()
if err != nil {
return err
err := cmd.Run()
if err != nil {
return err
}
}

err = t.WriteChangeID(ctx, &vmware.ChangeID{})
Expand Down
Loading