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