123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- /*
- Copyright 2014 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.
- */
- package gcepd
- import (
- "fmt"
- "path/filepath"
- "regexp"
- "strings"
- "time"
- "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/util/sets"
- cloudprovider "k8s.io/cloud-provider"
- cloudvolume "k8s.io/cloud-provider/volume"
- volumehelpers "k8s.io/cloud-provider/volume/helpers"
- "k8s.io/klog"
- "k8s.io/kubernetes/pkg/util/mount"
- "k8s.io/kubernetes/pkg/volume"
- volumeutil "k8s.io/kubernetes/pkg/volume/util"
- gcecloud "k8s.io/legacy-cloud-providers/gce"
- "k8s.io/utils/exec"
- utilpath "k8s.io/utils/path"
- )
- const (
- diskByIDPath = "/dev/disk/by-id/"
- diskGooglePrefix = "google-"
- diskScsiGooglePrefix = "scsi-0Google_PersistentDisk_"
- diskPartitionSuffix = "-part"
- diskSDPath = "/dev/sd"
- diskSDPattern = "/dev/sd*"
- maxRetries = 10
- checkSleepDuration = time.Second
- maxRegionalPDZones = 2
- // Replication type constants must be lower case.
- replicationTypeNone = "none"
- replicationTypeRegionalPD = "regional-pd"
- // scsi_id output should be in the form of:
- // 0Google PersistentDisk <disk name>
- scsiPattern = `^0Google\s+PersistentDisk\s+([\S]+)\s*$`
- )
- var (
- // errorSleepDuration is modified only in unit tests and should be constant
- // otherwise.
- errorSleepDuration = 5 * time.Second
- // regex to parse scsi_id output and extract the serial
- scsiRegex = regexp.MustCompile(scsiPattern)
- )
- // GCEDiskUtil provides operation for GCE PD
- type GCEDiskUtil struct{}
- // DeleteVolume deletes a GCE PD
- // Returns: error
- func (util *GCEDiskUtil) DeleteVolume(d *gcePersistentDiskDeleter) error {
- cloud, err := getCloudProvider(d.gcePersistentDisk.plugin.host.GetCloudProvider())
- if err != nil {
- return err
- }
- if err = cloud.DeleteDisk(d.pdName); err != nil {
- klog.V(2).Infof("Error deleting GCE PD volume %s: %v", d.pdName, err)
- // GCE cloud provider returns volume.deletedVolumeInUseError when
- // necessary, no handling needed here.
- return err
- }
- klog.V(2).Infof("Successfully deleted GCE PD volume %s", d.pdName)
- return nil
- }
- // CreateVolume creates a GCE PD.
- // Returns: gcePDName, volumeSizeGB, labels, fsType, error
- func (util *GCEDiskUtil) CreateVolume(c *gcePersistentDiskProvisioner, node *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (string, int, map[string]string, string, error) {
- cloud, err := getCloudProvider(c.gcePersistentDisk.plugin.host.GetCloudProvider())
- if err != nil {
- return "", 0, nil, "", err
- }
- name := volumeutil.GenerateVolumeName(c.options.ClusterName, c.options.PVName, 63) // GCE PD name can have up to 63 characters
- capacity := c.options.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)]
- // GCE PDs are allocated in chunks of GiBs
- requestGB := volumehelpers.RoundUpToGiB(capacity)
- // Apply Parameters.
- // Values for parameter "replication-type" are canonicalized to lower case.
- // Values for other parameters are case-insensitive, and we leave validation of these values
- // to the cloud provider.
- diskType := ""
- configuredZone := ""
- var configuredZones sets.String
- zonePresent := false
- zonesPresent := false
- replicationType := replicationTypeNone
- fstype := ""
- for k, v := range c.options.Parameters {
- switch strings.ToLower(k) {
- case "type":
- diskType = v
- case "zone":
- zonePresent = true
- configuredZone = v
- case "zones":
- zonesPresent = true
- configuredZones, err = volumehelpers.ZonesToSet(v)
- if err != nil {
- return "", 0, nil, "", err
- }
- case "replication-type":
- replicationType = strings.ToLower(v)
- case volume.VolumeParameterFSType:
- fstype = v
- default:
- return "", 0, nil, "", fmt.Errorf("invalid option %q for volume plugin %s", k, c.plugin.GetPluginName())
- }
- }
- // TODO: implement PVC.Selector parsing
- if c.options.PVC.Spec.Selector != nil {
- return "", 0, nil, "", fmt.Errorf("claim.Spec.Selector is not supported for dynamic provisioning on GCE")
- }
- var activezones sets.String
- activezones, err = cloud.GetAllCurrentZones()
- if err != nil {
- return "", 0, nil, "", err
- }
- switch replicationType {
- case replicationTypeRegionalPD:
- selectedZones, err := volumehelpers.SelectZonesForVolume(zonePresent, zonesPresent, configuredZone, configuredZones, activezones, node, allowedTopologies, c.options.PVC.Name, maxRegionalPDZones)
- if err != nil {
- klog.V(2).Infof("Error selecting zones for regional GCE PD volume: %v", err)
- return "", 0, nil, "", err
- }
- if err = cloud.CreateRegionalDisk(
- name,
- diskType,
- selectedZones,
- int64(requestGB),
- *c.options.CloudTags); err != nil {
- klog.V(2).Infof("Error creating regional GCE PD volume: %v", err)
- return "", 0, nil, "", err
- }
- klog.V(2).Infof("Successfully created Regional GCE PD volume %s", name)
- case replicationTypeNone:
- selectedZone, err := volumehelpers.SelectZoneForVolume(zonePresent, zonesPresent, configuredZone, configuredZones, activezones, node, allowedTopologies, c.options.PVC.Name)
- if err != nil {
- return "", 0, nil, "", err
- }
- if err := cloud.CreateDisk(
- name,
- diskType,
- selectedZone,
- int64(requestGB),
- *c.options.CloudTags); err != nil {
- klog.V(2).Infof("Error creating single-zone GCE PD volume: %v", err)
- return "", 0, nil, "", err
- }
- klog.V(2).Infof("Successfully created single-zone GCE PD volume %s", name)
- default:
- return "", 0, nil, "", fmt.Errorf("replication-type of '%s' is not supported", replicationType)
- }
- labels, err := cloud.GetAutoLabelsForPD(name, "" /* zone */)
- if err != nil {
- // We don't really want to leak the volume here...
- klog.Errorf("error getting labels for volume %q: %v", name, err)
- }
- return name, int(requestGB), labels, fstype, nil
- }
- // Returns the first path that exists, or empty string if none exist.
- func verifyDevicePath(devicePaths []string, sdBeforeSet sets.String, diskName string) (string, error) {
- if err := udevadmChangeToNewDrives(sdBeforeSet); err != nil {
- // It's possible udevadm was called on other disks so it should not block this
- // call. If it did fail on this disk, then the devicePath will either
- // not exist or be wrong. If it's wrong, then the scsi_id check below will fail.
- klog.Errorf("udevadmChangeToNewDrives failed with: %v", err)
- }
- for _, path := range devicePaths {
- if pathExists, err := mount.PathExists(path); err != nil {
- return "", fmt.Errorf("Error checking if path exists: %v", err)
- } else if pathExists {
- // validate that the path actually resolves to the correct disk
- serial, err := getScsiSerial(path, diskName)
- if err != nil {
- return "", fmt.Errorf("failed to get scsi serial %v", err)
- }
- if serial != diskName {
- // The device link is not pointing to the correct device
- // Trigger udev on this device to try to fix the link
- if udevErr := udevadmChangeToDrive(path); udevErr != nil {
- klog.Errorf("udevadmChangeToDrive %q failed with: %v", path, err)
- }
- // Return error to retry WaitForAttach and verifyDevicePath
- return "", fmt.Errorf("scsi_id serial %q for device %q doesn't match disk %q", serial, path, diskName)
- }
- // The device link is correct
- return path, nil
- }
- }
- return "", nil
- }
- // Calls scsi_id on the given devicePath to get the serial number reported by that device.
- func getScsiSerial(devicePath, diskName string) (string, error) {
- exists, err := utilpath.Exists(utilpath.CheckFollowSymlink, "/lib/udev/scsi_id")
- if err != nil {
- return "", fmt.Errorf("failed to check scsi_id existence: %v", err)
- }
- if !exists {
- klog.V(6).Infof("scsi_id doesn't exist; skipping check for %v", devicePath)
- return diskName, nil
- }
- out, err := exec.New().Command(
- "/lib/udev/scsi_id",
- "--page=0x83",
- "--whitelisted",
- fmt.Sprintf("--device=%v", devicePath)).CombinedOutput()
- if err != nil {
- return "", fmt.Errorf("scsi_id failed for device %q with %v", devicePath, err)
- }
- return parseScsiSerial(string(out))
- }
- // Parse the output returned by scsi_id and extract the serial number
- func parseScsiSerial(output string) (string, error) {
- substrings := scsiRegex.FindStringSubmatch(output)
- if substrings == nil {
- return "", fmt.Errorf("scsi_id output cannot be parsed: %q", output)
- }
- return substrings[1], nil
- }
- // Returns list of all /dev/disk/by-id/* paths for given PD.
- func getDiskByIDPaths(pdName string, partition string) []string {
- devicePaths := []string{
- filepath.Join(diskByIDPath, diskGooglePrefix+pdName),
- filepath.Join(diskByIDPath, diskScsiGooglePrefix+pdName),
- }
- if partition != "" {
- for i, path := range devicePaths {
- devicePaths[i] = path + diskPartitionSuffix + partition
- }
- }
- return devicePaths
- }
- // Return cloud provider
- func getCloudProvider(cloudProvider cloudprovider.Interface) (*gcecloud.Cloud, error) {
- var err error
- for numRetries := 0; numRetries < maxRetries; numRetries++ {
- gceCloudProvider, ok := cloudProvider.(*gcecloud.Cloud)
- if !ok || gceCloudProvider == nil {
- // Retry on error. See issue #11321
- klog.Errorf("Failed to get GCE Cloud Provider. plugin.host.GetCloudProvider returned %v instead", cloudProvider)
- time.Sleep(errorSleepDuration)
- continue
- }
- return gceCloudProvider, nil
- }
- return nil, fmt.Errorf("Failed to get GCE GCECloudProvider with error %v", err)
- }
- // Triggers the application of udev rules by calling "udevadm trigger
- // --action=change" for newly created "/dev/sd*" drives (exist only in
- // after set). This is workaround for Issue #7972. Once the underlying
- // issue has been resolved, this may be removed.
- func udevadmChangeToNewDrives(sdBeforeSet sets.String) error {
- sdAfter, err := filepath.Glob(diskSDPattern)
- if err != nil {
- return fmt.Errorf("error filepath.Glob(\"%s\"): %v\r", diskSDPattern, err)
- }
- for _, sd := range sdAfter {
- if !sdBeforeSet.Has(sd) {
- return udevadmChangeToDrive(sd)
- }
- }
- return nil
- }
- // Calls "udevadm trigger --action=change" on the specified drive.
- // drivePath must be the block device path to trigger on, in the format "/dev/sd*", or a symlink to it.
- // This is workaround for Issue #7972. Once the underlying issue has been resolved, this may be removed.
- func udevadmChangeToDrive(drivePath string) error {
- klog.V(5).Infof("udevadmChangeToDrive: drive=%q", drivePath)
- // Evaluate symlink, if any
- drive, err := filepath.EvalSymlinks(drivePath)
- if err != nil {
- return fmt.Errorf("udevadmChangeToDrive: filepath.EvalSymlinks(%q) failed with %v", drivePath, err)
- }
- klog.V(5).Infof("udevadmChangeToDrive: symlink path is %q", drive)
- // Check to make sure input is "/dev/sd*"
- if !strings.Contains(drive, diskSDPath) {
- return fmt.Errorf("udevadmChangeToDrive: expected input in the form \"%s\" but drive is %q", diskSDPattern, drive)
- }
- // Call "udevadm trigger --action=change --property-match=DEVNAME=/dev/sd..."
- _, err = exec.New().Command(
- "udevadm",
- "trigger",
- "--action=change",
- fmt.Sprintf("--property-match=DEVNAME=%s", drive)).CombinedOutput()
- if err != nil {
- return fmt.Errorf("udevadmChangeToDrive: udevadm trigger failed for drive %q with %v", drive, err)
- }
- return nil
- }
- // Checks whether the given GCE PD volume spec is associated with a regional PD.
- func isRegionalPD(spec *volume.Spec) bool {
- if spec.PersistentVolume != nil {
- zonesLabel := spec.PersistentVolume.Labels[v1.LabelZoneFailureDomain]
- zones := strings.Split(zonesLabel, cloudvolume.LabelMultiZoneDelimiter)
- return len(zones) > 1
- }
- return false
- }
|