From c1020cf8ceaa14121a1dac91d8dea41dea6d05b0 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 20 Feb 2026 11:13:59 +0100 Subject: [PATCH] feat: sanitize VM names for OpenStack resource naming Extend SafeVmName to transliterate common non-ASCII characters (Spanish, Portuguese, French, German), collapse consecutive underscores, truncate to 64 characters, and strip trailing underscores. Apply it consistently across migrate, create_server and create_network_port. --- plugins/module_utils/utils.go | 61 ++++++++++++++++++- .../create_network_port.go | 3 +- .../src/create_server/create_server.go | 5 +- plugins/modules/src/migrate/migrate.go | 4 +- tests/unit/utils_test.go | 38 ++++++++++-- 5 files changed, 101 insertions(+), 10 deletions(-) diff --git a/plugins/module_utils/utils.go b/plugins/module_utils/utils.go index 0e7bd8cf..63cf4214 100644 --- a/plugins/module_utils/utils.go +++ b/plugins/module_utils/utils.go @@ -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 } diff --git a/plugins/modules/src/create_network_port/create_network_port.go b/plugins/modules/src/create_network_port/create_network_port.go index e79e959f..ded061a1 100644 --- a/plugins/modules/src/create_network_port/create_network_port.go +++ b/plugins/modules/src/create_network_port/create_network_port.go @@ -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" ) @@ -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 { diff --git a/plugins/modules/src/create_server/create_server.go b/plugins/modules/src/create_server/create_server.go index 44e65dbd..1487d4ed 100644 --- a/plugins/modules/src/create_server/create_server.go +++ b/plugins/modules/src/create_server/create_server.go @@ -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" @@ -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, @@ -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, diff --git a/plugins/modules/src/migrate/migrate.go b/plugins/modules/src/migrate/migrate.go index f12500b5..0aa9c5eb 100644 --- a/plugins/modules/src/migrate/migrate.go +++ b/plugins/modules/src/migrate/migrate.go @@ -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 @@ -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, diff --git a/tests/unit/utils_test.go b/tests/unit/utils_test.go index 99e7c651..e7978083 100644 --- a/tests/unit/utils_test.go +++ b/tests/unit/utils_test.go @@ -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"}, + {"Á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 {