blob: f3b85e9a145c072e0286094e056609eb00defba2 [file] [log] [blame]
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// This version of Photon cloud provider supports the disk interface
// for Photon persistent disk volume plugin. LoadBalancer, Routes, and
// Zones are currently not supported.
// The use of Photon cloud provider requires to start kubelet, kube-apiserver,
// and kube-controller-manager with config flag: '--cloud-provider=photon
// --cloud-config=[path_to_config_file]'. When running multi-node kubernetes
// using docker, the config file should be located inside /etc/kubernetes.
package photon
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"strings"
"github.com/vmware/photon-controller-go-sdk/photon"
"gopkg.in/gcfg.v1"
"k8s.io/api/core/v1"
k8stypes "k8s.io/apimachinery/pkg/types"
cloudprovider "k8s.io/cloud-provider"
nodehelpers "k8s.io/cloud-provider/node/helpers"
"k8s.io/klog"
)
const (
ProviderName = "photon"
DiskSpecKind = "persistent-disk"
MAC_OUI_VC = "00:50:56"
MAC_OUI_ESX = "00:0c:29"
)
// overrideIP indicates if the hostname is overridden by IP address, such as when
// running multi-node kubernetes using docker. In this case the user should set
// overrideIP = true in cloud config file. Default value is false.
var overrideIP = false
var _ cloudprovider.Interface = (*PCCloud)(nil)
var _ cloudprovider.Instances = (*PCCloud)(nil)
var _ cloudprovider.Zones = (*PCCloud)(nil)
// PCCloud is an implementation of the cloud provider interface for Photon Controller.
type PCCloud struct {
cfg *PCConfig
// InstanceID of the server where this PCCloud object is instantiated.
localInstanceID string
// local $HOSTNAME
localHostname string
// hostname from K8S, could be overridden
localK8sHostname string
// Photon project ID. We assume that there is only one Photon Controller project
// in the environment per current Photon Controller deployment methodology.
projID string
cloudprovider.Zone
photonClient *photon.Client
logger *log.Logger
}
type PCConfig struct {
Global struct {
// the Photon Controller endpoint IP address
CloudTarget string `gcfg:"target"`
// Photon Controller project name
Project string `gcfg:"project"`
// when kubelet is started with '--hostname-override=${IP_ADDRESS}', set to true;
// otherwise, set to false.
OverrideIP bool `gcfg:"overrideIP"`
// VM ID for this node
VMID string `gcfg:"vmID"`
// Authentication enabled or not
AuthEnabled bool `gcfg:"authentication"`
}
}
// Disks is interface for manipulation with PhotonController Persistent Disks.
type Disks interface {
// AttachDisk attaches given disk to given node. Current node
// is used when nodeName is empty string.
AttachDisk(ctx context.Context, pdID string, nodeName k8stypes.NodeName) error
// DetachDisk detaches given disk to given node. Current node
// is used when nodeName is empty string.
DetachDisk(ctx context.Context, pdID string, nodeName k8stypes.NodeName) error
// DiskIsAttached checks if a disk is attached to the given node.
DiskIsAttached(ctx context.Context, pdID string, nodeName k8stypes.NodeName) (bool, error)
// DisksAreAttached is a batch function to check if a list of disks are attached
// to the node with the specified NodeName.
DisksAreAttached(ctx context.Context, pdIDs []string, nodeName k8stypes.NodeName) (map[string]bool, error)
// CreateDisk creates a new PD with given properties.
CreateDisk(volumeOptions *VolumeOptions) (pdID string, err error)
// DeleteDisk deletes PD.
DeleteDisk(pdID string) error
}
// VolumeOptions specifies capacity, tags, name and flavorID for a volume.
type VolumeOptions struct {
CapacityGB int
Tags map[string]string
Name string
Flavor string
}
func readConfig(config io.Reader) (PCConfig, error) {
if config == nil {
err := fmt.Errorf("cloud provider config file is missing. Please restart kubelet with --cloud-provider=photon --cloud-config=[path_to_config_file]")
return PCConfig{}, err
}
var cfg PCConfig
err := gcfg.ReadInto(&cfg, config)
return cfg, err
}
func init() {
cloudprovider.RegisterCloudProvider(ProviderName, func(config io.Reader) (cloudprovider.Interface, error) {
cfg, err := readConfig(config)
if err != nil {
klog.Errorf("Photon Cloud Provider: failed to read in cloud provider config file. Error[%v]", err)
return nil, err
}
return newPCCloud(cfg)
})
}
// Retrieve the Photon VM ID from the Photon Controller endpoint based on the node name
func getVMIDbyNodename(pc *PCCloud, nodeName string) (string, error) {
photonClient, err := getPhotonClient(pc)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to get photon client for getVMIDbyNodename, error: [%v]", err)
return "", err
}
vmList, err := photonClient.Projects.GetVMs(pc.projID, nil)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to GetVMs from project %s with nodeName %s, error: [%v]", pc.projID, nodeName, err)
return "", err
}
for _, vm := range vmList.Items {
if vm.Name == nodeName {
return vm.ID, nil
}
}
return "", fmt.Errorf("No matching started VM is found with name %s", nodeName)
}
// Retrieve the Photon VM ID from the Photon Controller endpoint based on the IP address
func getVMIDbyIP(pc *PCCloud, IPAddress string) (string, error) {
photonClient, err := getPhotonClient(pc)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to get photon client for getVMIDbyNodename, error: [%v]", err)
return "", err
}
vmList, err := photonClient.Projects.GetVMs(pc.projID, nil)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to GetVMs for project %s. error: [%v]", pc.projID, err)
return "", err
}
for _, vm := range vmList.Items {
task, err := photonClient.VMs.GetNetworks(vm.ID)
if err != nil {
klog.Warningf("Photon Cloud Provider: GetNetworks failed for vm.ID %s, error [%v]", vm.ID, err)
} else {
task, err = photonClient.Tasks.Wait(task.ID)
if err != nil {
klog.Warningf("Photon Cloud Provider: Wait task for GetNetworks failed for vm.ID %s, error [%v]", vm.ID, err)
} else {
networkConnections := task.ResourceProperties.(map[string]interface{})
networks := networkConnections["networkConnections"].([]interface{})
for _, nt := range networks {
network := nt.(map[string]interface{})
if val, ok := network["ipAddress"]; ok && val != nil {
ipAddr := val.(string)
if ipAddr == IPAddress {
return vm.ID, nil
}
}
}
}
}
}
return "", fmt.Errorf("No matching VM is found with IP %s", IPAddress)
}
func getPhotonClient(pc *PCCloud) (*photon.Client, error) {
var err error
if len(pc.cfg.Global.CloudTarget) == 0 {
return nil, fmt.Errorf("Photon Controller endpoint was not specified")
}
options := &photon.ClientOptions{
IgnoreCertificate: true,
}
pc.photonClient = photon.NewClient(pc.cfg.Global.CloudTarget, options, pc.logger)
if pc.cfg.Global.AuthEnabled == true {
// work around before metadata is available
file, err := os.Open("/etc/kubernetes/pc_login_info")
if err != nil {
klog.Errorf("Photon Cloud Provider: Authentication is enabled but found no username/password at /etc/kubernetes/pc_login_info. Error[%v]", err)
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
if !scanner.Scan() {
klog.Error("Photon Cloud Provider: Empty username inside /etc/kubernetes/pc_login_info.")
return nil, fmt.Errorf("Failed to create authentication enabled client with invalid username")
}
username := scanner.Text()
if !scanner.Scan() {
klog.Error("Photon Cloud Provider: Empty password set inside /etc/kubernetes/pc_login_info.")
return nil, fmt.Errorf("Failed to create authentication enabled client with invalid password")
}
password := scanner.Text()
tokenOptions, err := pc.photonClient.Auth.GetTokensByPassword(username, password)
if err != nil {
klog.Error("Photon Cloud Provider: failed to get tokens by password")
return nil, err
}
options = &photon.ClientOptions{
IgnoreCertificate: true,
TokenOptions: &photon.TokenOptions{
AccessToken: tokenOptions.AccessToken,
},
}
pc.photonClient = photon.NewClient(pc.cfg.Global.CloudTarget, options, pc.logger)
}
status, err := pc.photonClient.Status.Get()
if err != nil {
klog.Errorf("Photon Cloud Provider: new client creation failed. Error[%v]", err)
return nil, err
}
klog.V(2).Infof("Photon Cloud Provider: Status of the new photon controller client: %v", status)
return pc.photonClient, nil
}
func newPCCloud(cfg PCConfig) (*PCCloud, error) {
projID := cfg.Global.Project
vmID := cfg.Global.VMID
// Get local hostname
hostname, err := os.Hostname()
if err != nil {
klog.Errorf("Photon Cloud Provider: get hostname failed. Error[%v]", err)
return nil, err
}
pc := PCCloud{
cfg: &cfg,
localInstanceID: vmID,
localHostname: hostname,
localK8sHostname: "",
projID: projID,
}
overrideIP = cfg.Global.OverrideIP
return &pc, nil
}
// Initialize passes a Kubernetes clientBuilder interface to the cloud provider
func (pc *PCCloud) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, stop <-chan struct{}) {
}
// Instances returns an implementation of Instances for Photon Controller.
func (pc *PCCloud) Instances() (cloudprovider.Instances, bool) {
return pc, true
}
// List is an implementation of Instances.List.
func (pc *PCCloud) List(filter string) ([]k8stypes.NodeName, error) {
return nil, nil
}
// NodeAddresses is an implementation of Instances.NodeAddresses.
func (pc *PCCloud) NodeAddresses(ctx context.Context, nodeName k8stypes.NodeName) ([]v1.NodeAddress, error) {
nodeAddrs := []v1.NodeAddress{}
name := string(nodeName)
if name == pc.localK8sHostname {
ifaces, err := net.Interfaces()
if err != nil {
klog.Errorf("Photon Cloud Provider: net.Interfaces() failed for NodeAddresses. Error[%v]", err)
return nodeAddrs, err
}
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
klog.Warningf("Photon Cloud Provider: Failed to extract addresses for NodeAddresses. Error[%v]", err)
} else {
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
// Filter external IP by MAC address OUIs from vCenter and from ESX
if strings.HasPrefix(i.HardwareAddr.String(), MAC_OUI_VC) ||
strings.HasPrefix(i.HardwareAddr.String(), MAC_OUI_ESX) {
nodehelpers.AddToNodeAddresses(&nodeAddrs,
v1.NodeAddress{
Type: v1.NodeExternalIP,
Address: ipnet.IP.String(),
},
)
} else {
nodehelpers.AddToNodeAddresses(&nodeAddrs,
v1.NodeAddress{
Type: v1.NodeInternalIP,
Address: ipnet.IP.String(),
},
)
}
}
}
}
}
}
return nodeAddrs, nil
}
// Inquiring IP addresses from photon controller endpoint only for a node other than this node.
// This is assumed to be done by master only.
vmID, err := getInstanceID(pc, name)
if err != nil {
klog.Errorf("Photon Cloud Provider: getInstanceID failed for NodeAddresses. Error[%v]", err)
return nodeAddrs, err
}
photonClient, err := getPhotonClient(pc)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to get photon client for NodeAddresses, error: [%v]", err)
return nodeAddrs, err
}
// Retrieve the Photon VM's IP addresses from the Photon Controller endpoint based on the VM ID
vmList, err := photonClient.Projects.GetVMs(pc.projID, nil)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to GetVMs for project %s. Error[%v]", pc.projID, err)
return nodeAddrs, err
}
for _, vm := range vmList.Items {
if vm.ID == vmID {
task, err := photonClient.VMs.GetNetworks(vm.ID)
if err != nil {
klog.Errorf("Photon Cloud Provider: GetNetworks failed for node %s with vm.ID %s. Error[%v]", name, vm.ID, err)
return nodeAddrs, err
} else {
task, err = photonClient.Tasks.Wait(task.ID)
if err != nil {
klog.Errorf("Photon Cloud Provider: Wait task for GetNetworks failed for node %s with vm.ID %s. Error[%v]", name, vm.ID, err)
return nodeAddrs, err
} else {
networkConnections := task.ResourceProperties.(map[string]interface{})
networks := networkConnections["networkConnections"].([]interface{})
for _, nt := range networks {
ipAddr := "-"
macAddr := "-"
network := nt.(map[string]interface{})
if val, ok := network["ipAddress"]; ok && val != nil {
ipAddr = val.(string)
}
if val, ok := network["macAddress"]; ok && val != nil {
macAddr = val.(string)
}
if ipAddr != "-" {
if strings.HasPrefix(macAddr, MAC_OUI_VC) ||
strings.HasPrefix(macAddr, MAC_OUI_ESX) {
nodehelpers.AddToNodeAddresses(&nodeAddrs,
v1.NodeAddress{
Type: v1.NodeExternalIP,
Address: ipAddr,
},
)
} else {
nodehelpers.AddToNodeAddresses(&nodeAddrs,
v1.NodeAddress{
Type: v1.NodeInternalIP,
Address: ipAddr,
},
)
}
}
}
return nodeAddrs, nil
}
}
}
}
klog.Errorf("Failed to find the node %s from Photon Controller endpoint", name)
return nodeAddrs, fmt.Errorf("Failed to find the node %s from Photon Controller endpoint", name)
}
// NodeAddressesByProviderID returns the node addresses of an instances with the specified unique providerID
// This method will not be called from the node that is requesting this ID. i.e. metadata service
// and other local methods cannot be used here
func (pc *PCCloud) NodeAddressesByProviderID(ctx context.Context, providerID string) ([]v1.NodeAddress, error) {
return []v1.NodeAddress{}, cloudprovider.NotImplemented
}
func (pc *PCCloud) AddSSHKeyToAllInstances(ctx context.Context, user string, keyData []byte) error {
return cloudprovider.NotImplemented
}
func (pc *PCCloud) CurrentNodeName(ctx context.Context, hostname string) (k8stypes.NodeName, error) {
pc.localK8sHostname = hostname
return k8stypes.NodeName(hostname), nil
}
func getInstanceID(pc *PCCloud, name string) (string, error) {
var vmID string
var err error
if overrideIP == true {
vmID, err = getVMIDbyIP(pc, name)
} else {
vmID, err = getVMIDbyNodename(pc, name)
}
if err != nil {
return "", err
}
if vmID == "" {
err = cloudprovider.InstanceNotFound
}
return vmID, err
}
// InstanceExistsByProviderID returns true if the instance with the given provider id still exists and is running.
// If false is returned with no error, the instance will be immediately deleted by the cloud controller manager.
func (pc *PCCloud) InstanceExistsByProviderID(ctx context.Context, providerID string) (bool, error) {
return false, cloudprovider.NotImplemented
}
// InstanceShutdownByProviderID returns true if the instance is in safe state to detach volumes
func (pc *PCCloud) InstanceShutdownByProviderID(ctx context.Context, providerID string) (bool, error) {
return false, cloudprovider.NotImplemented
}
// InstanceID returns the cloud provider ID of the specified instance.
func (pc *PCCloud) InstanceID(ctx context.Context, nodeName k8stypes.NodeName) (string, error) {
name := string(nodeName)
if name == pc.localK8sHostname {
return pc.localInstanceID, nil
}
// We assume only master need to get InstanceID of a node other than itself
id, err := getInstanceID(pc, name)
if err != nil {
klog.Errorf("Photon Cloud Provider: getInstanceID failed for InstanceID. Error[%v]", err)
}
return id, err
}
// InstanceTypeByProviderID returns the cloudprovider instance type of the node with the specified unique providerID
// This method will not be called from the node that is requesting this ID. i.e. metadata service
// and other local methods cannot be used here
func (pc *PCCloud) InstanceTypeByProviderID(ctx context.Context, providerID string) (string, error) {
return "", cloudprovider.NotImplemented
}
func (pc *PCCloud) InstanceType(ctx context.Context, nodeName k8stypes.NodeName) (string, error) {
return "", nil
}
func (pc *PCCloud) Clusters() (cloudprovider.Clusters, bool) {
return nil, true
}
// ProviderName returns the cloud provider ID.
func (pc *PCCloud) ProviderName() string {
return ProviderName
}
// LoadBalancer returns an implementation of LoadBalancer for Photon Controller.
func (pc *PCCloud) LoadBalancer() (cloudprovider.LoadBalancer, bool) {
return nil, false
}
// Zones returns an implementation of Zones for Photon Controller.
func (pc *PCCloud) Zones() (cloudprovider.Zones, bool) {
return pc, true
}
func (pc *PCCloud) GetZone(ctx context.Context) (cloudprovider.Zone, error) {
return pc.Zone, nil
}
// GetZoneByProviderID implements Zones.GetZoneByProviderID
// This is particularly useful in external cloud providers where the kubelet
// does not initialize node data.
func (pc *PCCloud) GetZoneByProviderID(ctx context.Context, providerID string) (cloudprovider.Zone, error) {
return cloudprovider.Zone{}, errors.New("GetZoneByProviderID not implemented")
}
// GetZoneByNodeName implements Zones.GetZoneByNodeName
// This is particularly useful in external cloud providers where the kubelet
// does not initialize node data.
func (pc *PCCloud) GetZoneByNodeName(ctx context.Context, nodeName k8stypes.NodeName) (cloudprovider.Zone, error) {
return cloudprovider.Zone{}, errors.New("GetZoneByNodeName not imeplemented")
}
// Routes returns a false since the interface is not supported for photon controller.
func (pc *PCCloud) Routes() (cloudprovider.Routes, bool) {
return nil, false
}
// HasClusterID returns true if the cluster has a clusterID
func (pc *PCCloud) HasClusterID() bool {
return true
}
// AttachDisk attaches given virtual disk volume to the compute running kubelet.
func (pc *PCCloud) AttachDisk(ctx context.Context, pdID string, nodeName k8stypes.NodeName) error {
photonClient, err := getPhotonClient(pc)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to get photon client for AttachDisk, error: [%v]", err)
return err
}
operation := &photon.VmDiskOperation{
DiskID: pdID,
}
vmID, err := pc.InstanceID(ctx, nodeName)
if err != nil {
klog.Errorf("Photon Cloud Provider: pc.InstanceID failed for AttachDisk. Error[%v]", err)
return err
}
task, err := photonClient.VMs.AttachDisk(vmID, operation)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to attach disk with pdID %s. Error[%v]", pdID, err)
return err
}
_, err = photonClient.Tasks.Wait(task.ID)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to wait for task to attach disk with pdID %s. Error[%v]", pdID, err)
return err
}
return nil
}
// Detaches given virtual disk volume from the compute running kubelet.
func (pc *PCCloud) DetachDisk(ctx context.Context, pdID string, nodeName k8stypes.NodeName) error {
photonClient, err := getPhotonClient(pc)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to get photon client for DetachDisk, error: [%v]", err)
return err
}
operation := &photon.VmDiskOperation{
DiskID: pdID,
}
vmID, err := pc.InstanceID(ctx, nodeName)
if err != nil {
klog.Errorf("Photon Cloud Provider: pc.InstanceID failed for DetachDisk. Error[%v]", err)
return err
}
task, err := photonClient.VMs.DetachDisk(vmID, operation)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to detach disk with pdID %s. Error[%v]", pdID, err)
return err
}
_, err = photonClient.Tasks.Wait(task.ID)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to wait for task to detach disk with pdID %s. Error[%v]", pdID, err)
return err
}
return nil
}
// DiskIsAttached returns if disk is attached to the VM using controllers supported by the plugin.
func (pc *PCCloud) DiskIsAttached(ctx context.Context, pdID string, nodeName k8stypes.NodeName) (bool, error) {
photonClient, err := getPhotonClient(pc)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to get photon client for DiskIsAttached, error: [%v]", err)
return false, err
}
disk, err := photonClient.Disks.Get(pdID)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to Get disk with pdID %s. Error[%v]", pdID, err)
return false, err
}
vmID, err := pc.InstanceID(ctx, nodeName)
if err == cloudprovider.InstanceNotFound {
klog.Infof("Instance %q does not exist, disk %s will be detached automatically.", nodeName, pdID)
return false, nil
}
if err != nil {
klog.Errorf("Photon Cloud Provider: pc.InstanceID failed for DiskIsAttached. Error[%v]", err)
return false, err
}
for _, vm := range disk.VMs {
if vm == vmID {
return true, nil
}
}
return false, nil
}
// DisksAreAttached returns if disks are attached to the VM using controllers supported by the plugin.
func (pc *PCCloud) DisksAreAttached(ctx context.Context, pdIDs []string, nodeName k8stypes.NodeName) (map[string]bool, error) {
attached := make(map[string]bool)
photonClient, err := getPhotonClient(pc)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to get photon client for DisksAreAttached, error: [%v]", err)
return attached, err
}
for _, pdID := range pdIDs {
attached[pdID] = false
}
vmID, err := pc.InstanceID(ctx, nodeName)
if err == cloudprovider.InstanceNotFound {
klog.Infof("Instance %q does not exist, its disks will be detached automatically.", nodeName)
// make all the disks as detached.
return attached, nil
}
if err != nil {
klog.Errorf("Photon Cloud Provider: pc.InstanceID failed for DiskIsAttached. Error[%v]", err)
return attached, err
}
for _, pdID := range pdIDs {
disk, err := photonClient.Disks.Get(pdID)
if err != nil {
klog.Warningf("Photon Cloud Provider: failed to get VMs for persistent disk %s, err [%v]", pdID, err)
} else {
for _, vm := range disk.VMs {
if vm == vmID {
attached[pdID] = true
}
}
}
}
return attached, nil
}
// Create a volume of given size (in GB).
func (pc *PCCloud) CreateDisk(volumeOptions *VolumeOptions) (pdID string, err error) {
photonClient, err := getPhotonClient(pc)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to get photon client for CreateDisk, error: [%v]", err)
return "", err
}
diskSpec := photon.DiskCreateSpec{}
diskSpec.Name = volumeOptions.Name
diskSpec.Flavor = volumeOptions.Flavor
diskSpec.CapacityGB = volumeOptions.CapacityGB
diskSpec.Kind = DiskSpecKind
task, err := photonClient.Projects.CreateDisk(pc.projID, &diskSpec)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to CreateDisk. Error[%v]", err)
return "", err
}
waitTask, err := photonClient.Tasks.Wait(task.ID)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to wait for task to CreateDisk. Error[%v]", err)
return "", err
}
return waitTask.Entity.ID, nil
}
// DeleteDisk deletes a volume given volume name.
func (pc *PCCloud) DeleteDisk(pdID string) error {
photonClient, err := getPhotonClient(pc)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to get photon client for DeleteDisk, error: [%v]", err)
return err
}
task, err := photonClient.Disks.Delete(pdID)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to DeleteDisk. Error[%v]", err)
return err
}
_, err = photonClient.Tasks.Wait(task.ID)
if err != nil {
klog.Errorf("Photon Cloud Provider: Failed to wait for task to DeleteDisk. Error[%v]", err)
return err
}
return nil
}