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