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  }