github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/vsphere/environ_broker.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package vsphere
     5  
     6  import (
     7  	"fmt"
     8  	"path"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/juju/clock"
    13  	"github.com/juju/errors"
    14  	"github.com/vmware/govmomi/vim25/mo"
    15  
    16  	"github.com/juju/juju/cloudconfig/cloudinit"
    17  	"github.com/juju/juju/cloudconfig/instancecfg"
    18  	"github.com/juju/juju/cloudconfig/providerinit"
    19  	corebase "github.com/juju/juju/core/base"
    20  	"github.com/juju/juju/core/instance"
    21  	corenetwork "github.com/juju/juju/core/network"
    22  	"github.com/juju/juju/core/os/ostype"
    23  	"github.com/juju/juju/core/status"
    24  	"github.com/juju/juju/environs"
    25  	"github.com/juju/juju/environs/context"
    26  	"github.com/juju/juju/environs/instances"
    27  	"github.com/juju/juju/environs/simplestreams"
    28  	"github.com/juju/juju/provider/common"
    29  	"github.com/juju/juju/provider/vsphere/internal/vsphereclient"
    30  	"github.com/juju/juju/tools"
    31  )
    32  
    33  const (
    34  	startInstanceUpdateProgressInterval = 30 * time.Second
    35  	bootstrapUpdateProgressInterval     = 5 * time.Second
    36  )
    37  
    38  func controllerFolderName(controllerUUID string) string {
    39  	return fmt.Sprintf("Juju Controller (%s)", controllerUUID)
    40  }
    41  
    42  func modelFolderName(modelUUID, modelName string) string {
    43  	// We must truncate model names at 33 characters, in order to keep the
    44  	// folder name to a maximum of 80 characters. The documentation says
    45  	// "less than 80", but testing shows that it is in fact "no more than 80".
    46  	//
    47  	// See https://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.Folder.html:
    48  	//   "The name to be given the new folder. An entity name must be
    49  	//   a non-empty string of less than 80 characters. The slash (/),
    50  	//   backslash (\) and percent (%) will be escaped using the URL
    51  	//   syntax. For example, %2F."
    52  	const modelNameLimit = 33
    53  	if len(modelName) > modelNameLimit {
    54  		modelName = modelName[:modelNameLimit]
    55  	}
    56  	return fmt.Sprintf("Model %q (%s)", modelName, modelUUID)
    57  }
    58  
    59  // templateDirectoryName returns the name of the datastore directory in which
    60  // the VM templates are stored for the controller.
    61  func templateDirectoryName(controllerFolderName string) string {
    62  	return path.Join(controllerFolderName, "templates")
    63  }
    64  
    65  // StartInstance implements environs.InstanceBroker.
    66  func (env *environ) StartInstance(ctx context.ProviderCallContext, args environs.StartInstanceParams) (result *environs.StartInstanceResult, err error) {
    67  	err = env.withSession(ctx, func(env *sessionEnviron) error {
    68  		result, err = env.StartInstance(ctx, args)
    69  		return err
    70  	})
    71  	return result, err
    72  }
    73  
    74  // Region is specified in the HasRegion interface.
    75  func (env *environ) Region() (simplestreams.CloudSpec, error) {
    76  	spec := simplestreams.CloudSpec{
    77  		Region:   env.cloud.Region,
    78  		Endpoint: env.cloud.Endpoint,
    79  	}
    80  	return spec, nil
    81  }
    82  
    83  // StartInstance implements environs.InstanceBroker.
    84  func (env *sessionEnviron) StartInstance(ctx context.ProviderCallContext, args environs.StartInstanceParams) (*environs.StartInstanceResult, error) {
    85  	vm, hw, err := env.newRawInstance(ctx, args)
    86  	if err != nil {
    87  		_ = args.StatusCallback(status.ProvisioningError, fmt.Sprint(err), nil)
    88  		return nil, errors.Trace(err)
    89  	}
    90  
    91  	logger.Infof("started instance %q", vm.Name)
    92  	logger.Tracef("instance data %+v", vm)
    93  	inst := newInstance(vm, env.environ)
    94  	result := environs.StartInstanceResult{
    95  		Instance: inst,
    96  		Hardware: hw,
    97  	}
    98  	return &result, nil
    99  }
   100  
   101  // FinishInstanceConfig is exported, because it has to be rewritten in external unit tests
   102  var FinishInstanceConfig = instancecfg.FinishInstanceConfig
   103  
   104  // finishMachineConfig updates args.MachineConfig in place. Setting up
   105  // the API, StateServing, and SSHkeys information.
   106  func (env *sessionEnviron) finishMachineConfig(args environs.StartInstanceParams, arch string) error {
   107  	envTools, err := args.Tools.Match(tools.Filter{Arch: arch})
   108  	if err != nil {
   109  		return err
   110  	}
   111  	if err := args.InstanceConfig.SetTools(envTools); err != nil {
   112  		return errors.Trace(err)
   113  	}
   114  	return FinishInstanceConfig(args.InstanceConfig, env.Config())
   115  }
   116  
   117  // newRawInstance is where the new physical instance is actually
   118  // provisioned, relative to the provided args and spec. Info for that
   119  // low-level instance is returned.
   120  func (env *sessionEnviron) newRawInstance(
   121  	ctx context.ProviderCallContext,
   122  	args environs.StartInstanceParams,
   123  ) (_ *mo.VirtualMachine, _ *instance.HardwareCharacteristics, err error) {
   124  	// Obtain the final constraints by merging with defaults.
   125  	cons := args.Constraints
   126  	os := ostype.OSTypeForName(args.InstanceConfig.Base.OS)
   127  	minRootDisk := common.MinRootDiskSizeGiB(os) * 1024
   128  	if cons.RootDisk == nil || *cons.RootDisk < minRootDisk {
   129  		cons.RootDisk = &minRootDisk
   130  	}
   131  
   132  	defaultDatastore := env.ecfg.datastore()
   133  	if cons.RootDiskSource == nil || *cons.RootDiskSource == "" {
   134  		cons.RootDiskSource = &defaultDatastore
   135  	}
   136  
   137  	// Attempt to create a VM in each of the AZs in turn.
   138  	logger.Debugf("attempting to create VM in availability zone %q", args.AvailabilityZone)
   139  	availZone, err := env.availZone(ctx, args.AvailabilityZone)
   140  	if err != nil {
   141  		return nil, nil, errors.Trace(err)
   142  	}
   143  
   144  	datastore, err := env.client.GetTargetDatastore(env.ctx, &availZone.r, *cons.RootDiskSource)
   145  	if err != nil {
   146  		return nil, nil, errors.Trace(err)
   147  	}
   148  
   149  	updateProgressInterval := startInstanceUpdateProgressInterval
   150  	if args.InstanceConfig.Bootstrap != nil {
   151  		updateProgressInterval = bootstrapUpdateProgressInterval
   152  	}
   153  	updateProgress := func(message string) {
   154  		_ = args.StatusCallback(status.Provisioning, message, nil)
   155  	}
   156  
   157  	statusUpdateArgs := vsphereclient.StatusUpdateParams{
   158  		UpdateProgress:         updateProgress,
   159  		UpdateProgressInterval: updateProgressInterval,
   160  		Clock:                  clock.WallClock,
   161  	}
   162  
   163  	tplManager := vmTemplateManager{
   164  		imageMetadata:    args.ImageMetadata,
   165  		env:              env.environ,
   166  		client:           env.client,
   167  		vmFolder:         env.getVMFolder(),
   168  		azPoolRef:        availZone.pool.Reference(),
   169  		datastore:        datastore,
   170  		controllerUUID:   args.ControllerUUID,
   171  		statusUpdateArgs: statusUpdateArgs,
   172  	}
   173  
   174  	arch, err := args.Tools.OneArch()
   175  	if err != nil {
   176  		return nil, nil, errors.Trace(err)
   177  	}
   178  	series, err := corebase.GetSeriesFromBase(args.InstanceConfig.Base)
   179  	if err != nil {
   180  		return nil, nil, errors.Trace(err)
   181  	}
   182  	vmTemplate, arch, err := tplManager.EnsureTemplate(env.ctx, series, arch)
   183  	if err != nil {
   184  		return nil, nil, environs.ZoneIndependentError(err)
   185  	}
   186  
   187  	if err := env.finishMachineConfig(args, arch); err != nil {
   188  		return nil, nil, environs.ZoneIndependentError(err)
   189  	}
   190  
   191  	if args.AvailabilityZone == "" {
   192  		return nil, nil, errors.NotValidf("empty available zone")
   193  	}
   194  
   195  	vmName, err := env.namespace.Hostname(args.InstanceConfig.MachineId)
   196  	if err != nil {
   197  		return nil, nil, environs.ZoneIndependentError(err)
   198  	}
   199  
   200  	cloudcfg, err := cloudinit.New(args.InstanceConfig.Base.OS)
   201  	if err != nil {
   202  		return nil, nil, environs.ZoneIndependentError(err)
   203  	}
   204  	cloudcfg.AddPackage("open-vm-tools")
   205  	cloudcfg.AddPackage("iptables-persistent")
   206  
   207  	// Make sure the hostname is resolvable by adding it to /etc/hosts.
   208  	cloudcfg.ManageEtcHosts(true)
   209  
   210  	internalMac, err := vsphereclient.GenerateMAC()
   211  	if err != nil {
   212  		return nil, nil, errors.Trace(err)
   213  	}
   214  
   215  	interfaces := corenetwork.InterfaceInfos{{
   216  		InterfaceName: "eth0",
   217  		MACAddress:    internalMac,
   218  		InterfaceType: corenetwork.EthernetDevice,
   219  		ConfigType:    corenetwork.ConfigDHCP,
   220  		Origin:        corenetwork.OriginProvider,
   221  	}}
   222  	networkDevices := []vsphereclient.NetworkDevice{{MAC: internalMac, Network: env.ecfg.primaryNetwork()}}
   223  
   224  	// TODO(wpk) We need to add a firewall -AND- make sure that if it's a controller we
   225  	// have API port open.
   226  	externalNetwork := env.ecfg.externalNetwork()
   227  	if externalNetwork != "" {
   228  		externalMac, err := vsphereclient.GenerateMAC()
   229  		if err != nil {
   230  			return nil, nil, errors.Trace(err)
   231  		}
   232  		interfaces = append(interfaces, corenetwork.InterfaceInfo{
   233  			InterfaceName: "eth1",
   234  			MACAddress:    externalMac,
   235  			InterfaceType: corenetwork.EthernetDevice,
   236  			ConfigType:    corenetwork.ConfigDHCP,
   237  			Origin:        corenetwork.OriginProvider,
   238  		})
   239  		networkDevices = append(networkDevices, vsphereclient.NetworkDevice{MAC: externalMac, Network: externalNetwork})
   240  	}
   241  	// TODO(wpk) There's no (known) way to tell cloud-init to disable network (using cloudinit.CloudInitNetworkConfigDisabled)
   242  	// so the network might be double-configured. That should be ok as long as we're using DHCP.
   243  	err = cloudcfg.AddNetworkConfig(interfaces)
   244  	if err != nil {
   245  		return nil, nil, errors.Trace(err)
   246  	}
   247  	userData, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg, VsphereRenderer{})
   248  	if err != nil {
   249  		return nil, nil, environs.ZoneIndependentError(
   250  			errors.Annotate(err, "cannot make user data"),
   251  		)
   252  	}
   253  	logger.Debugf("Vmware user data; %d bytes", len(userData))
   254  
   255  	createVMArgs := vsphereclient.CreateVirtualMachineParams{
   256  		Name:                   vmName,
   257  		Folder:                 path.Join(env.getVMFolder(), controllerFolderName(args.ControllerUUID), env.modelFolderName()),
   258  		Series:                 series,
   259  		UserData:               string(userData),
   260  		Metadata:               args.InstanceConfig.Tags,
   261  		Constraints:            cons,
   262  		NetworkDevices:         networkDevices,
   263  		EnableDiskUUID:         env.ecfg.enableDiskUUID(),
   264  		ForceVMHardwareVersion: env.ecfg.forceVMHardwareVersion(),
   265  		DiskProvisioningType:   env.ecfg.diskProvisioningType(),
   266  		StatusUpdateParams:     statusUpdateArgs,
   267  		Datastore:              datastore,
   268  		VMTemplate:             vmTemplate,
   269  		ComputeResource:        &availZone.r,
   270  		ResourcePool:           availZone.pool.Reference(),
   271  	}
   272  
   273  	vm, err := env.client.CreateVirtualMachine(env.ctx, createVMArgs)
   274  	if vsphereclient.IsExtendDiskError(err) {
   275  		// Ensure we don't try to make the same extension across
   276  		// different resource groups.
   277  		err = environs.ZoneIndependentError(err)
   278  	}
   279  	if err != nil {
   280  		HandleCredentialError(err, env, ctx)
   281  		return nil, nil, errors.Trace(err)
   282  
   283  	}
   284  
   285  	hw := &instance.HardwareCharacteristics{
   286  		Arch:           &arch,
   287  		Mem:            cons.Mem,
   288  		CpuCores:       cons.CpuCores,
   289  		CpuPower:       cons.CpuPower,
   290  		RootDisk:       cons.RootDisk,
   291  		RootDiskSource: cons.RootDiskSource,
   292  	}
   293  	return vm, hw, err
   294  }
   295  
   296  // AllInstances implements environs.InstanceBroker.
   297  func (env *environ) AllInstances(ctx context.ProviderCallContext) (instances []instances.Instance, err error) {
   298  	err = env.withSession(ctx, func(env *sessionEnviron) error {
   299  		instances, err = env.AllInstances(ctx)
   300  		return err
   301  	})
   302  	return instances, err
   303  }
   304  
   305  // AllInstances implements environs.InstanceBroker.
   306  func (env *sessionEnviron) AllInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) {
   307  	modelFolderPath := path.Join(env.getVMFolder(), controllerFolderName("*"), env.modelFolderName())
   308  	vms, err := env.client.VirtualMachines(env.ctx, modelFolderPath+"/*")
   309  	if err != nil {
   310  		HandleCredentialError(err, env, ctx)
   311  		return nil, errors.Trace(err)
   312  	}
   313  
   314  	var results []instances.Instance
   315  	for _, vm := range vms {
   316  		results = append(results, newInstance(vm, env.environ))
   317  	}
   318  	return results, err
   319  }
   320  
   321  // AllRunningInstances implements environs.InstanceBroker.
   322  func (env *environ) AllRunningInstances(ctx context.ProviderCallContext) (instances []instances.Instance, err error) {
   323  	// AllInstances() already handles all instances irrespective of the state, so
   324  	// here 'all' is also 'all running'.
   325  	return env.AllInstances(ctx)
   326  }
   327  
   328  // AllRunningInstances implements environs.InstanceBroker.
   329  func (env *sessionEnviron) AllRunningInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) {
   330  	// AllInstances() already handles all instances irrespective of the state, so
   331  	// here 'all' is also 'all running'.
   332  	return env.AllInstances(ctx)
   333  }
   334  
   335  // StopInstances implements environs.InstanceBroker.
   336  func (env *environ) StopInstances(ctx context.ProviderCallContext, ids ...instance.Id) error {
   337  	return env.withSession(ctx, func(env *sessionEnviron) error {
   338  		return env.StopInstances(ctx, ids...)
   339  	})
   340  }
   341  
   342  // StopInstances implements environs.InstanceBroker.
   343  func (env *sessionEnviron) StopInstances(ctx context.ProviderCallContext, ids ...instance.Id) error {
   344  	modelFolderPath := path.Join(env.getVMFolder(), controllerFolderName("*"), env.modelFolderName())
   345  	results := make([]error, len(ids))
   346  	var wg sync.WaitGroup
   347  	for i, id := range ids {
   348  		wg.Add(1)
   349  		go func(i int, id instance.Id) {
   350  			defer wg.Done()
   351  			results[i] = env.client.RemoveVirtualMachines(
   352  				env.ctx,
   353  				path.Join(modelFolderPath, string(id)),
   354  			)
   355  			HandleCredentialError(results[i], env, ctx)
   356  		}(i, id)
   357  	}
   358  	wg.Wait()
   359  
   360  	var errIds []instance.Id
   361  	var errs []error
   362  	for i, err := range results {
   363  		if err != nil {
   364  			errIds = append(errIds, ids[i])
   365  			errs = append(errs, err)
   366  		}
   367  	}
   368  	switch len(errs) {
   369  	case 0:
   370  		return nil
   371  	case 1:
   372  		return errors.Annotatef(errs[0], "failed to stop instance %s", errIds[0])
   373  	default:
   374  		return errors.Errorf(
   375  			"failed to stop instances %s: %s",
   376  			errIds, errs,
   377  		)
   378  	}
   379  }