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 }