aws_util.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /*
  2. Copyright 2014 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package awsebs
  14. import (
  15. "fmt"
  16. "os"
  17. "path/filepath"
  18. "strconv"
  19. "strings"
  20. "time"
  21. "k8s.io/klog"
  22. "k8s.io/api/core/v1"
  23. "k8s.io/apimachinery/pkg/api/resource"
  24. "k8s.io/apimachinery/pkg/util/sets"
  25. "k8s.io/cloud-provider"
  26. volumehelpers "k8s.io/cloud-provider/volume/helpers"
  27. "k8s.io/kubernetes/pkg/util/mount"
  28. "k8s.io/kubernetes/pkg/volume"
  29. volumeutil "k8s.io/kubernetes/pkg/volume/util"
  30. "k8s.io/legacy-cloud-providers/aws"
  31. )
  32. const (
  33. diskPartitionSuffix = ""
  34. checkSleepDuration = time.Second
  35. )
  36. // AWSDiskUtil provides operations for EBS volume.
  37. type AWSDiskUtil struct{}
  38. // DeleteVolume deletes an AWS EBS volume.
  39. func (util *AWSDiskUtil) DeleteVolume(d *awsElasticBlockStoreDeleter) error {
  40. cloud, err := getCloudProvider(d.awsElasticBlockStore.plugin.host.GetCloudProvider())
  41. if err != nil {
  42. return err
  43. }
  44. deleted, err := cloud.DeleteDisk(d.volumeID)
  45. if err != nil {
  46. // AWS cloud provider returns volume.deletedVolumeInUseError when
  47. // necessary, no handling needed here.
  48. klog.V(2).Infof("Error deleting EBS Disk volume %s: %v", d.volumeID, err)
  49. return err
  50. }
  51. if deleted {
  52. klog.V(2).Infof("Successfully deleted EBS Disk volume %s", d.volumeID)
  53. } else {
  54. klog.V(2).Infof("Successfully deleted EBS Disk volume %s (actually already deleted)", d.volumeID)
  55. }
  56. return nil
  57. }
  58. // CreateVolume creates an AWS EBS volume.
  59. // Returns: volumeID, volumeSizeGB, labels, error
  60. func (util *AWSDiskUtil) CreateVolume(c *awsElasticBlockStoreProvisioner, node *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (aws.KubernetesVolumeID, int, map[string]string, string, error) {
  61. cloud, err := getCloudProvider(c.awsElasticBlockStore.plugin.host.GetCloudProvider())
  62. if err != nil {
  63. return "", 0, nil, "", err
  64. }
  65. // AWS volumes don't have Name field, store the name in Name tag
  66. var tags map[string]string
  67. if c.options.CloudTags == nil {
  68. tags = make(map[string]string)
  69. } else {
  70. tags = *c.options.CloudTags
  71. }
  72. tags["Name"] = volumeutil.GenerateVolumeName(c.options.ClusterName, c.options.PVName, 255) // AWS tags can have 255 characters
  73. capacity := c.options.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)]
  74. zonesWithNodes, err := getCandidateZones(cloud, node)
  75. if err != nil {
  76. return "", 0, nil, "", fmt.Errorf("error finding candidate zone for pvc: %v", err)
  77. }
  78. volumeOptions, err := populateVolumeOptions(c.plugin.GetPluginName(), c.options.PVC.Name, capacity, tags, c.options.Parameters, node, allowedTopologies, zonesWithNodes)
  79. if err != nil {
  80. klog.V(2).Infof("Error populating EBS options: %v", err)
  81. return "", 0, nil, "", err
  82. }
  83. // TODO: implement PVC.Selector parsing
  84. if c.options.PVC.Spec.Selector != nil {
  85. return "", 0, nil, "", fmt.Errorf("claim.Spec.Selector is not supported for dynamic provisioning on AWS")
  86. }
  87. name, err := cloud.CreateDisk(volumeOptions)
  88. if err != nil {
  89. klog.V(2).Infof("Error creating EBS Disk volume: %v", err)
  90. return "", 0, nil, "", err
  91. }
  92. klog.V(2).Infof("Successfully created EBS Disk volume %s", name)
  93. labels, err := cloud.GetVolumeLabels(name)
  94. if err != nil {
  95. // We don't really want to leak the volume here...
  96. klog.Errorf("error building labels for new EBS volume %q: %v", name, err)
  97. }
  98. fstype := ""
  99. for k, v := range c.options.Parameters {
  100. if strings.ToLower(k) == volume.VolumeParameterFSType {
  101. fstype = v
  102. }
  103. }
  104. return name, volumeOptions.CapacityGB, labels, fstype, nil
  105. }
  106. // getCandidateZones finds possible zones that a volume can be created in
  107. func getCandidateZones(cloud *aws.Cloud, selectedNode *v1.Node) (sets.String, error) {
  108. if selectedNode != nil {
  109. // For topology aware volume provisioning, node is already selected so we use the zone from
  110. // selected node directly instead of candidate zones.
  111. // We can assume the information is always available as node controller shall maintain it.
  112. return sets.NewString(), nil
  113. }
  114. // For non-topology-aware volumes (those that binds immediately), we fall back to original logic to query
  115. // cloud provider for possible zones
  116. return cloud.GetCandidateZonesForDynamicVolume()
  117. }
  118. // returns volumeOptions for EBS based on storageclass parameters and node configuration
  119. func populateVolumeOptions(pluginName, pvcName string, capacityGB resource.Quantity, tags map[string]string, storageParams map[string]string, node *v1.Node, allowedTopologies []v1.TopologySelectorTerm, zonesWithNodes sets.String) (*aws.VolumeOptions, error) {
  120. requestGiB, err := volumehelpers.RoundUpToGiBInt(capacityGB)
  121. if err != nil {
  122. return nil, err
  123. }
  124. volumeOptions := &aws.VolumeOptions{
  125. CapacityGB: requestGiB,
  126. Tags: tags,
  127. }
  128. // Apply Parameters (case-insensitive). We leave validation of
  129. // the values to the cloud provider.
  130. zonePresent := false
  131. zonesPresent := false
  132. var zone string
  133. var zones sets.String
  134. for k, v := range storageParams {
  135. switch strings.ToLower(k) {
  136. case "type":
  137. volumeOptions.VolumeType = v
  138. case "zone":
  139. zonePresent = true
  140. zone = v
  141. case "zones":
  142. zonesPresent = true
  143. zones, err = volumehelpers.ZonesToSet(v)
  144. if err != nil {
  145. return nil, fmt.Errorf("error parsing zones %s, must be strings separated by commas: %v", zones, err)
  146. }
  147. case "iopspergb":
  148. volumeOptions.IOPSPerGB, err = strconv.Atoi(v)
  149. if err != nil {
  150. return nil, fmt.Errorf("invalid iopsPerGB value %q, must be integer between 1 and 30: %v", v, err)
  151. }
  152. case "encrypted":
  153. volumeOptions.Encrypted, err = strconv.ParseBool(v)
  154. if err != nil {
  155. return nil, fmt.Errorf("invalid encrypted boolean value %q, must be true or false: %v", v, err)
  156. }
  157. case "kmskeyid":
  158. volumeOptions.KmsKeyID = v
  159. case volume.VolumeParameterFSType:
  160. // Do nothing but don't make this fail
  161. default:
  162. return nil, fmt.Errorf("invalid option %q for volume plugin %s", k, pluginName)
  163. }
  164. }
  165. volumeOptions.AvailabilityZone, err = volumehelpers.SelectZoneForVolume(zonePresent, zonesPresent, zone, zones, zonesWithNodes, node, allowedTopologies, pvcName)
  166. if err != nil {
  167. return nil, err
  168. }
  169. return volumeOptions, nil
  170. }
  171. // Returns the first path that exists, or empty string if none exist.
  172. func verifyDevicePath(devicePaths []string) (string, error) {
  173. for _, path := range devicePaths {
  174. if pathExists, err := mount.PathExists(path); err != nil {
  175. return "", fmt.Errorf("Error checking if path exists: %v", err)
  176. } else if pathExists {
  177. return path, nil
  178. }
  179. }
  180. return "", nil
  181. }
  182. // Returns list of all paths for given EBS mount
  183. // This is more interesting on GCE (where we are able to identify volumes under /dev/disk-by-id)
  184. // Here it is mostly about applying the partition path
  185. func getDiskByIDPaths(volumeID aws.KubernetesVolumeID, partition string, devicePath string) []string {
  186. devicePaths := []string{}
  187. if devicePath != "" {
  188. devicePaths = append(devicePaths, devicePath)
  189. }
  190. if partition != "" {
  191. for i, path := range devicePaths {
  192. devicePaths[i] = path + diskPartitionSuffix + partition
  193. }
  194. }
  195. // We need to find NVME volumes, which are mounted on a "random" nvme path ("/dev/nvme0n1"),
  196. // and we have to get the volume id from the nvme interface
  197. awsVolumeID, err := volumeID.MapToAWSVolumeID()
  198. if err != nil {
  199. klog.Warningf("error mapping volume %q to AWS volume: %v", volumeID, err)
  200. } else {
  201. // This is the magic name on which AWS presents NVME devices under /dev/disk/by-id/
  202. // For example, vol-0fab1d5e3f72a5e23 creates a symlink at /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol0fab1d5e3f72a5e23
  203. nvmeName := "nvme-Amazon_Elastic_Block_Store_" + strings.Replace(string(awsVolumeID), "-", "", -1)
  204. nvmePath, err := findNvmeVolume(nvmeName)
  205. if err != nil {
  206. klog.Warningf("error looking for nvme volume %q: %v", volumeID, err)
  207. } else if nvmePath != "" {
  208. devicePaths = append(devicePaths, nvmePath)
  209. }
  210. }
  211. return devicePaths
  212. }
  213. // Return cloud provider
  214. func getCloudProvider(cloudProvider cloudprovider.Interface) (*aws.Cloud, error) {
  215. awsCloudProvider, ok := cloudProvider.(*aws.Cloud)
  216. if !ok || awsCloudProvider == nil {
  217. return nil, fmt.Errorf("Failed to get AWS Cloud Provider. GetCloudProvider returned %v instead", cloudProvider)
  218. }
  219. return awsCloudProvider, nil
  220. }
  221. // findNvmeVolume looks for the nvme volume with the specified name
  222. // It follows the symlink (if it exists) and returns the absolute path to the device
  223. func findNvmeVolume(findName string) (device string, err error) {
  224. p := filepath.Join("/dev/disk/by-id/", findName)
  225. stat, err := os.Lstat(p)
  226. if err != nil {
  227. if os.IsNotExist(err) {
  228. klog.V(6).Infof("nvme path not found %q", p)
  229. return "", nil
  230. }
  231. return "", fmt.Errorf("error getting stat of %q: %v", p, err)
  232. }
  233. if stat.Mode()&os.ModeSymlink != os.ModeSymlink {
  234. klog.Warningf("nvme file %q found, but was not a symlink", p)
  235. return "", nil
  236. }
  237. // Find the target, resolving to an absolute path
  238. // For example, /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol0fab1d5e3f72a5e23 -> ../../nvme2n1
  239. resolved, err := filepath.EvalSymlinks(p)
  240. if err != nil {
  241. return "", fmt.Errorf("error reading target of symlink %q: %v", p, err)
  242. }
  243. if !strings.HasPrefix(resolved, "/dev") {
  244. return "", fmt.Errorf("resolved symlink for %q was unexpected: %q", p, resolved)
  245. }
  246. return resolved, nil
  247. }