github.com/vmware/govmomi@v0.51.0/cli/vm/customize.go (about) 1 // © Broadcom. All Rights Reserved. 2 // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. 3 // SPDX-License-Identifier: Apache-2.0 4 5 package vm 6 7 import ( 8 "context" 9 "flag" 10 "fmt" 11 "strconv" 12 "strings" 13 14 "github.com/vmware/govmomi/cli" 15 "github.com/vmware/govmomi/cli/flags" 16 "github.com/vmware/govmomi/object" 17 "github.com/vmware/govmomi/vim25/types" 18 ) 19 20 type customize struct { 21 *flags.VirtualMachineFlag 22 23 alc int 24 prefix types.CustomizationPrefixName 25 tz string 26 domain string 27 host types.CustomizationFixedName 28 mac flags.StringList 29 ip flags.StringList 30 ip6 flags.StringList 31 gateway flags.StringList 32 netmask flags.StringList 33 dnsserver flags.StringList 34 dnssuffix flags.StringList 35 kind string 36 username string 37 org string 38 } 39 40 func init() { 41 cli.Register("vm.customize", &customize{}) 42 } 43 44 func (cmd *customize) Register(ctx context.Context, f *flag.FlagSet) { 45 cmd.VirtualMachineFlag, ctx = flags.NewVirtualMachineFlag(ctx) 46 cmd.VirtualMachineFlag.Register(ctx, f) 47 48 f.IntVar(&cmd.alc, "auto-login", 0, "Number of times the VM should automatically login as an administrator") 49 f.StringVar(&cmd.prefix.Base, "prefix", "", "Host name generator prefix") 50 f.StringVar(&cmd.tz, "tz", "", "Time zone") 51 f.StringVar(&cmd.domain, "domain", "", "Domain name") 52 f.StringVar(&cmd.host.Name, "name", "", "Host name") 53 f.Var(&cmd.mac, "mac", "MAC address") 54 cmd.mac = nil 55 f.Var(&cmd.ip, "ip", "IPv4 address") 56 cmd.ip = nil 57 f.Var(&cmd.ip6, "ip6", "IPv6 addresses with optional netmask (defaults to /64), separated by comma") 58 cmd.ip6 = nil 59 f.Var(&cmd.gateway, "gateway", "Gateway") 60 cmd.gateway = nil 61 f.Var(&cmd.netmask, "netmask", "Netmask") 62 cmd.netmask = nil 63 f.Var(&cmd.dnsserver, "dns-server", "DNS server list") 64 cmd.dnsserver = nil 65 f.Var(&cmd.dnssuffix, "dns-suffix", "DNS suffix list") 66 cmd.dnssuffix = nil 67 f.StringVar(&cmd.kind, "type", "Linux", "Customization type if spec NAME is not specified (Linux|Windows)") 68 f.StringVar(&cmd.username, "username", "", "Windows only : full name of the end user in firstname lastname format") 69 f.StringVar(&cmd.org, "org", "", "Windows only : name of the org that owns the VM") 70 } 71 72 func (cmd *customize) Usage() string { 73 return "[NAME]" 74 } 75 76 func (cmd *customize) Description() string { 77 return `Customize VM. 78 79 Optionally specify a customization spec NAME. 80 81 The '-ip', '-netmask' and '-gateway' flags are for static IP configuration. 82 If the VM has multiple NICs, an '-ip' and '-netmask' must be specified for each. 83 84 The '-dns-server' and '-dns-suffix' flags can be specified multiple times. 85 86 Windows -tz value requires the Index (hex): https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values 87 88 Examples: 89 govc vm.customize -vm VM NAME 90 govc vm.customize -vm VM -name my-hostname -ip dhcp 91 govc vm.customize -vm VM -gateway GATEWAY -ip NEWIP -netmask NETMASK -dns-server DNS1,DNS2 NAME 92 # Multiple -ip without -mac are applied by vCenter in the order in which the NICs appear on the bus 93 govc vm.customize -vm VM -ip 10.0.0.178 -netmask 255.255.255.0 -ip 10.0.0.162 -netmask 255.255.255.0 94 # Multiple -ip with -mac are applied by vCenter to the NIC with the given MAC address 95 govc vm.customize -vm VM -mac 00:50:56:be:dd:f8 -ip 10.0.0.178 -netmask 255.255.255.0 -mac 00:50:56:be:60:cf -ip 10.0.0.162 -netmask 255.255.255.0 96 # Dual stack IPv4/IPv6, single NIC 97 govc vm.customize -vm VM -ip 10.0.0.1 -netmask 255.255.255.0 -ip6 '2001:db8::1/64' -name my-hostname NAME 98 # DHCPv6, single NIC 99 govc vm.customize -vm VM -ip6 dhcp6 NAME 100 # Static IPv6, three NICs, last one with two addresses 101 govc vm.customize -vm VM -ip6 2001:db8::1/64 -ip6 2001:db8::2/64 -ip6 2001:db8::3/64,2001:db8::4/64 NAME 102 govc vm.customize -vm VM -auto-login 3 NAME 103 govc vm.customize -vm VM -prefix demo NAME 104 govc vm.customize -vm VM -tz America/New_York NAME` 105 } 106 107 // Parse a string of multiple IPv6 addresses with optional netmask; separated by comma 108 func parseIPv6Argument(argv string) (ipconf []types.BaseCustomizationIpV6Generator, err error) { 109 for _, substring := range strings.Split(argv, ",") { 110 // remove leading and trailing white space 111 substring = strings.TrimSpace(substring) 112 // handle "dhcp6" and lists of static IPv6 addresses 113 switch substring { 114 case "dhcp6": 115 ipconf = append( 116 ipconf, 117 &types.CustomizationDhcpIpV6Generator{}, 118 ) 119 default: 120 // check if subnet mask was specified 121 switch strings.Count(substring, "/") { 122 // no mask, set default 123 case 0: 124 ipconf = append(ipconf, &types.CustomizationFixedIpV6{ 125 IpAddress: substring, 126 SubnetMask: 64, 127 }) 128 // a single forward slash was found: parse and use subnet mask 129 case 1: 130 parts := strings.Split(substring, "/") 131 mask, err := strconv.Atoi(parts[1]) 132 if err != nil { 133 return nil, fmt.Errorf("unable to convert subnet mask to int: %w", err) 134 } 135 ipconf = append(ipconf, &types.CustomizationFixedIpV6{ 136 IpAddress: parts[0], 137 SubnetMask: int32(mask), 138 }) 139 // too many forward slashes; return error 140 default: 141 return nil, fmt.Errorf("unable to parse IPv6 address (too many subnet separators): %s", substring) 142 } 143 } 144 } 145 return ipconf, nil 146 } 147 148 func (cmd *customize) Run(ctx context.Context, f *flag.FlagSet) error { 149 vm, err := cmd.VirtualMachineFlag.VirtualMachine() 150 if err != nil { 151 return err 152 } 153 154 if vm == nil { 155 return flag.ErrHelp 156 } 157 158 var spec *types.CustomizationSpec 159 160 name := f.Arg(0) 161 if name == "" { 162 spec = &types.CustomizationSpec{ 163 NicSettingMap: make([]types.CustomizationAdapterMapping, len(cmd.ip)), 164 } 165 166 switch cmd.kind { 167 case "Linux": 168 spec.Identity = &types.CustomizationLinuxPrep{ 169 HostName: new(types.CustomizationVirtualMachineName), 170 } 171 case "Windows": 172 spec.Identity = &types.CustomizationSysprep{ 173 UserData: types.CustomizationUserData{ 174 ComputerName: new(types.CustomizationVirtualMachineName), 175 }, 176 } 177 default: 178 return flag.ErrHelp 179 } 180 } else { 181 m := object.NewCustomizationSpecManager(vm.Client()) 182 183 exists, err := m.DoesCustomizationSpecExist(ctx, name) 184 if err != nil { 185 return err 186 } 187 if !exists { 188 return fmt.Errorf("specification %q does not exist", name) 189 } 190 191 item, err := m.GetCustomizationSpec(ctx, name) 192 if err != nil { 193 return err 194 } 195 196 spec = &item.Spec 197 } 198 199 if len(cmd.ip) > len(spec.NicSettingMap) { 200 return fmt.Errorf("%d -ip specified, spec %q has %d", len(cmd.ip), name, len(spec.NicSettingMap)) 201 } 202 203 sysprep, isWindows := spec.Identity.(*types.CustomizationSysprep) 204 linprep, _ := spec.Identity.(*types.CustomizationLinuxPrep) 205 206 if isWindows { 207 sysprep.Identification.JoinDomain = cmd.domain 208 sysprep.UserData.FullName = cmd.username 209 sysprep.UserData.OrgName = cmd.org 210 } else { 211 linprep.Domain = cmd.domain 212 } 213 214 if len(cmd.dnsserver) != 0 { 215 if !isWindows { 216 for _, s := range cmd.dnsserver { 217 spec.GlobalIPSettings.DnsServerList = 218 append(spec.GlobalIPSettings.DnsServerList, strings.Split(s, ",")...) 219 } 220 } 221 } 222 223 spec.GlobalIPSettings.DnsSuffixList = cmd.dnssuffix 224 225 if cmd.prefix.Base != "" { 226 if isWindows { 227 sysprep.UserData.ComputerName = &cmd.prefix 228 } else { 229 linprep.HostName = &cmd.prefix 230 } 231 } 232 233 if cmd.host.Name != "" { 234 if isWindows { 235 sysprep.UserData.ComputerName = &cmd.host 236 } else { 237 linprep.HostName = &cmd.host 238 } 239 } 240 241 if cmd.alc != 0 { 242 if !isWindows { 243 return fmt.Errorf("option '-auto-login' is Windows only") 244 } 245 sysprep.GuiUnattended.AutoLogon = true 246 sysprep.GuiUnattended.AutoLogonCount = int32(cmd.alc) 247 } 248 249 if cmd.tz != "" { 250 if isWindows { 251 tz, err := strconv.ParseInt(cmd.tz, 16, 32) 252 if err != nil { 253 return fmt.Errorf("converting -tz=%q: %s", cmd.tz, err) 254 } 255 sysprep.GuiUnattended.TimeZone = int32(tz) 256 } else { 257 linprep.TimeZone = cmd.tz 258 } 259 } 260 261 for i, ip := range cmd.ip { 262 nic := &spec.NicSettingMap[i] 263 switch ip { 264 case "dhcp": 265 nic.Adapter.Ip = new(types.CustomizationDhcpIpGenerator) 266 default: 267 nic.Adapter.Ip = &types.CustomizationFixedIp{IpAddress: ip} 268 } 269 270 if i < len(cmd.netmask) { 271 nic.Adapter.SubnetMask = cmd.netmask[i] 272 } 273 if i < len(cmd.mac) { 274 nic.MacAddress = cmd.mac[i] 275 } 276 if i < len(cmd.gateway) { 277 nic.Adapter.Gateway = strings.Split(cmd.gateway[i], ",") 278 } 279 if isWindows { 280 if i < len(cmd.dnsserver) { 281 nic.Adapter.DnsServerList = strings.Split(cmd.dnsserver[i], ",") 282 } 283 } 284 } 285 286 for i, ip6 := range cmd.ip6 { 287 ipconfig, err := parseIPv6Argument(ip6) 288 if err != nil { 289 return err 290 } 291 // use the same logic as the ip switch: the first occurrence of the ip6 switch is assigned to the first nic, 292 // the second to the second nic and so forth. 293 if spec.NicSettingMap == nil || len(spec.NicSettingMap) < i { 294 return fmt.Errorf("unable to find a network adapter for IPv6 settings %d (%s)", i, ip6) 295 } 296 nic := &spec.NicSettingMap[i] 297 if nic.Adapter.IpV6Spec == nil { 298 nic.Adapter.IpV6Spec = new(types.CustomizationIPSettingsIpV6AddressSpec) 299 } 300 nic.Adapter.IpV6Spec.Ip = append(nic.Adapter.IpV6Spec.Ip, ipconfig...) 301 } 302 303 task, err := vm.Customize(ctx, *spec) 304 if err != nil { 305 return err 306 } 307 308 return task.Wait(ctx) 309 }