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 }