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
61 changes: 60 additions & 1 deletion plugins/module_utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,66 @@ func GenRandom(length int) (string, error) {
return string(result), nil
}

// transliterations maps common non-ASCII characters to their ASCII equivalents.
// Covers Spanish, Portuguese, French, German, and extended Latin characters.
var transliterations = map[rune]string{
// Spanish
'á': "a", 'é': "e", 'í': "i", 'ó': "o", 'ú': "u",
'Á': "A", 'É': "E", 'Í': "I", 'Ó': "O", 'Ú': "U",
'ñ': "n", 'Ñ': "N", 'ü': "u", 'Ü': "U",
// Portuguese
'ã': "a", 'õ': "o", 'à': "a", 'â': "a",
'ê': "e", 'ô': "o", 'ç': "c",
'Ã': "A", 'Õ': "O", 'À': "A", 'Â': "A",
'Ê': "E", 'Ô': "O", 'Ç': "C",
// French
'è': "e", 'ë': "e", 'î': "i", 'ï': "i",
'ù': "u", 'û': "u",
'È': "E", 'Ë': "E", 'Î': "I", 'Ï': "I",
'Ù': "U", 'Û': "U",
'æ': "ae", 'Æ': "AE", 'œ': "oe", 'Œ': "OE",
// German
'ä': "a", 'ö': "o",
'Ä': "A", 'Ö': "O",
'ß': "ss",
// Extended Latin
'ì': "i", 'ò': "o",
'Ì': "I", 'Ò': "O",
// Common special characters
'·': "_", // interpunct
'\u2019': "_", // right single quotation mark
'\u2013': "_", // en-dash
'\u2014': "_", // em-dash
'\u00A0': "_", // non-breaking space
}

// transliterate replaces known non-ASCII runes with their ASCII equivalents.
func transliterate(vmName string) string {
var b strings.Builder
for _, r := range vmName {
if replacement, ok := transliterations[r]; ok {
b.WriteString(replacement)
} else {
b.WriteRune(r)
}
}
return b.String()
}

// SafeVmName sanitizes a VMware VM name for use as an OpenStack resource name.
// It transliterates common non-ASCII characters, replaces any remaining
// non-alphanumeric characters (except underscore) with underscores, collapses
// consecutive underscores into one, truncates to 64 characters, and strips
// any trailing underscores left after truncation.
func SafeVmName(vmName string) string {
safe := transliterate(vmName)
re := regexp.MustCompile(`[^a-zA-Z0-9_]`)
return re.ReplaceAllString(vmName, "_")
safe = re.ReplaceAllString(safe, "_")
multi := regexp.MustCompile(`_+`)
safe = multi.ReplaceAllString(safe, "_")
if len(safe) > 64 {
safe = safe[:64]
}
safe = strings.TrimRight(safe, "_")
return safe
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"io"
"os"
moduleutils "vmware-migration-kit/plugins/module_utils"
"vmware-migration-kit/plugins/module_utils/logger"
osm_os "vmware-migration-kit/plugins/module_utils/openstack"
)
Expand Down Expand Up @@ -181,7 +182,7 @@ func main() {
}
}
}
portName := fmt.Sprintf("%s-NIC-%d-VLAN-%s", moduleArgs.VmName, nicIndex, nic.Vlan)
portName := fmt.Sprintf("%s-NIC-%d-VLAN-%s", moduleutils.SafeVmName(moduleArgs.VmName), nicIndex, nic.Vlan)
port, err := osm_os.CreatePort(provider, portName, network.ID, nic.Mac, nic.Subnet,
moduleArgs.SecurityGroups, nic.FixedIPs)
if err != nil {
Expand Down
5 changes: 3 additions & 2 deletions plugins/modules/src/create_server/create_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"encoding/json"
"os"
moduleutils "vmware-migration-kit/plugins/module_utils"
"vmware-migration-kit/plugins/module_utils/ansible"
"vmware-migration-kit/plugins/module_utils/logger"
osm_os "vmware-migration-kit/plugins/module_utils/openstack"
Expand Down Expand Up @@ -153,7 +154,7 @@ func main() {
}

ServerAgrs := osm_os.ServerArgs{
Name: moduleArgs.Name,
Name: moduleutils.SafeVmName(moduleArgs.Name),
Flavor: moduleArgs.Flavor,
BootVolume: moduleArgs.BootVolume,
SecurityGroups: moduleArgs.SecurityGroups,
Expand All @@ -172,7 +173,7 @@ func main() {
}

ServerAgrs := osm_os.ServerArgs{
Name: moduleArgs.Name,
Name: moduleutils.SafeVmName(moduleArgs.Name),
Flavor: moduleArgs.Flavor,
BootVolume: moduleArgs.BootVolume,
SecurityGroups: moduleArgs.SecurityGroups,
Expand Down
4 changes: 2 additions & 2 deletions plugins/modules/src/migrate/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func (c *MigrationConfig) VMMigration(parentCtx context.Context, runV2V bool) (s
return "", err
}
diskNameStr := strconv.Itoa(int(c.NbdkitConfig.VddkConfig.DiskKey))
volume, err := osm_os.GetVolumeID(c.OSClient, vmName, diskNameStr)
volume, err := osm_os.GetVolumeID(c.OSClient, moduleutils.SafeVmName(vmName), diskNameStr)
if err != nil {
logger.Log.Infof("Failed to get volume: %v", err)
return "", err
Expand Down Expand Up @@ -223,7 +223,7 @@ func (c *MigrationConfig) VMMigration(parentCtx context.Context, runV2V bool) (s
// volumeMetadata["hw_scsi_model"] = "virtio-scsi"
// }
volOpts := osm_os.VolOpts{
Name: vmName + "-" + diskNameStr,
Name: moduleutils.SafeVmName(vmName) + "-" + diskNameStr,
Size: int(diskSize[diskNameStr] / 1024 / 1024),
VolumeType: c.VolumeType,
AvailabilityZone: c.VolumeAz,
Expand Down
38 changes: 34 additions & 4 deletions tests/unit/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,19 +214,49 @@ func TestFindDevName_BrokenSymlink(t *testing.T) {
}
}

// Test 11: SafeVmName sanitizes VM names for shell safety
// Test 11: SafeVmName sanitizes VM names for use as OpenStack resource names
func TestSafeVmName(t *testing.T) {
tests := []struct {
input string
expected string
}{
// Basic cases (existing)
{"vm1", "vm1"},
{"vm-01", "vm_01"},
{"My VM", "My_VM"},
{"vm@#$%", "vm____"},
{"_already_ok_", "_already_ok_"},
{" ", "_"},
{"vm@#$%", "vm"},
{"_already_ok_", "_already_ok"},
{" ", ""},
{"Mi-VM.2025", "Mi_VM_2025"},
// Underscore collapsing
{"vm--01", "vm_01"},
{"vm...test", "vm_test"},
{"VM prod", "VM_prod"},
{"vm___server", "vm_server"},
// Transliteration - Spanish
{"ñoño", "nono"},
Comment thread
fdiazbra marked this conversation as resolved.
{"Ángel_García", "Angel_Garcia"},
{"Açaí_VM", "Acai_VM"},
{"Muñoz-Server", "Munoz_Server"},
{"Ñoño_Server", "Nono_Server"},
// Transliteration - accented vowels
{"áéíóú", "aeiou"},
{"ÁÉÍÓÚ", "AEIOU"},
{"São_Paulo", "Sao_Paulo"},
{"Château_prod", "Chateau_prod"},
// Transliteration - German
{"Müller_vm", "Muller_vm"},
{"straße_01", "strasse_01"},
// Transliteration - French
{"Hôtel_de_ville", "Hotel_de_ville"},
// Truncation to 64 characters
{strings.Repeat("a", 70), strings.Repeat("a", 64)},
{strings.Repeat("a", 64), strings.Repeat("a", 64)},
{strings.Repeat("a", 63), strings.Repeat("a", 63)},
// Trailing underscore stripped after truncation
{"vm_" + strings.Repeat("a", 60) + "!!!!", "vm_" + strings.Repeat("a", 60)},
// Combination: transliteration + collapsing + truncation
{"ñ" + strings.Repeat("a", 70), "n" + strings.Repeat("a", 63)},
}

for _, tt := range tests {
Expand Down