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