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  }