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  }