diff --git a/pkg/volume/awsebs/BUILD b/pkg/volume/awsebs/BUILD index 59eae87fc92..5e21539885d 100644 --- a/pkg/volume/awsebs/BUILD +++ b/pkg/volume/awsebs/BUILD @@ -10,6 +10,9 @@ go_library( name = "go_default_library", srcs = [ "attacher.go", + "attacher_linux.go", + "attacher_unsupported.go", + "attacher_windows.go", "aws_ebs.go", "aws_ebs_block.go", "aws_util.go", diff --git a/pkg/volume/awsebs/attacher.go b/pkg/volume/awsebs/attacher.go index 603180b7e42..c939af5aae5 100644 --- a/pkg/volume/awsebs/attacher.go +++ b/pkg/volume/awsebs/attacher.go @@ -20,6 +20,8 @@ import ( "fmt" "os" "path" + "path/filepath" + "runtime" "strconv" "time" @@ -175,15 +177,14 @@ func (attacher *awsElasticBlockStoreAttacher) WaitForAttach(spec *volume.Spec, d for { select { case <-ticker.C: - klog.V(5).Infof("Checking AWS Volume %q is attached.", volumeID) - devicePaths := getDiskByIDPaths(aws.KubernetesVolumeID(volumeSource.VolumeID), partition, devicePath) - path, err := verifyDevicePath(devicePaths) + klog.V(5).Infof("Checking AWS Volume %q is attached at devicePath %q.", volumeID, devicePath) + path, err := attacher.getDevicePath(volumeSource.VolumeID, partition, devicePath) if err != nil { // Log error, if any, and continue checking periodically. See issue #11321 - klog.Errorf("Error verifying AWS Volume (%q) is attached: %v", volumeID, err) + klog.Errorf("Error verifying AWS Volume (%q) is attached at devicePath %q: %v", volumeID, devicePath, err) } else if path != "" { // A device path has successfully been created for the PD - klog.Infof("Successfully found attached AWS Volume %q.", volumeID) + klog.Infof("Successfully found attached AWS Volume %q at path %q.", volumeID, path) return path, nil } case <-timer.C: @@ -208,8 +209,17 @@ func (attacher *awsElasticBlockStoreAttacher) MountDevice(spec *volume.Spec, dev notMnt, err := mounter.IsLikelyNotMountPoint(deviceMountPath) if err != nil { if os.IsNotExist(err) { - if err := os.MkdirAll(deviceMountPath, 0750); err != nil { - return err + dir := deviceMountPath + if runtime.GOOS == "windows" { + // On Windows, FormatAndMount will mklink (create a symbolic link) at deviceMountPath later, so don't create a + // directory at deviceMountPath now. Otherwise mklink will error: "Cannot create a file when that file already exists". + // Instead, create the parent of deviceMountPath. For example when deviceMountPath is: + // C:\var\lib\kubelet\plugins\kubernetes.io\aws-ebs\mounts\aws\us-west-2b\vol-xxx + // create us-west-2b. FormatAndMount will make vol-xxx a symlink to the drive (e.g. D:\) + dir = filepath.Dir(deviceMountPath) + } + if err := os.MkdirAll(dir, 0750); err != nil { + return fmt.Errorf("making dir %s failed with %s", dir, err) } notMnt = true } else { diff --git a/pkg/volume/awsebs/attacher_linux.go b/pkg/volume/awsebs/attacher_linux.go new file mode 100644 index 00000000000..562a567ca34 --- /dev/null +++ b/pkg/volume/awsebs/attacher_linux.go @@ -0,0 +1,28 @@ +// +build linux + +/* +Copyright 2019 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 awsebs + +import ( + "k8s.io/legacy-cloud-providers/aws" +) + +func (attacher *awsElasticBlockStoreAttacher) getDevicePath(volumeID, partition, devicePath string) (string, error) { + devicePaths := getDiskByIDPaths(aws.KubernetesVolumeID(volumeID), partition, devicePath) + return verifyDevicePath(devicePaths) +} diff --git a/pkg/volume/awsebs/attacher_unsupported.go b/pkg/volume/awsebs/attacher_unsupported.go new file mode 100644 index 00000000000..efbd78acf0c --- /dev/null +++ b/pkg/volume/awsebs/attacher_unsupported.go @@ -0,0 +1,25 @@ +// +build !linux,!windows + +/* +Copyright 2019 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 awsebs + +import "errors" + +func (attacher *awsElasticBlockStoreAttacher) getDevicePath(volumeID, partition, devicePath string) (string, error) { + return "", errors.New("unsupported") +} diff --git a/pkg/volume/awsebs/attacher_windows.go b/pkg/volume/awsebs/attacher_windows.go new file mode 100644 index 00000000000..6ad160410c2 --- /dev/null +++ b/pkg/volume/awsebs/attacher_windows.go @@ -0,0 +1,68 @@ +// +build windows + +/* +Copyright 2019 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 awsebs + +import ( + "fmt" + "regexp" + "strings" +) + +// ebsnvme-id is present on AWS-provided Windows Server AMIs +// https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/nvme-ebs-volumes.html#identify-nvme-ebs-device +const ebsnvmeID = `C:\ProgramData\Amazon\Tools\ebsnvme-id.exe` + +func (attacher *awsElasticBlockStoreAttacher) getDevicePath(volumeID, partition, devicePath string) (string, error) { + return attacher.getDiskNumber(volumeID) +} + +// getDiskNumber gets the Windows disk number for a given volume ID. The disk number is needed for mounting. +// TODO This only works for Nitro-based instances +// TODO fallback to Get-Disk +func (attacher *awsElasticBlockStoreAttacher) getDiskNumber(volumeID string) (string, error) { + // Split the ID from zone: aws://us-west-2b/vol-06d0909eb358b05f9 + split := strings.Split(volumeID, "/") + volumeID = split[len(split)-1] + + exec := attacher.host.GetExec(awsElasticBlockStorePluginName) + output, err := exec.Run(ebsnvmeID) + if err != nil { + return "", fmt.Errorf("error calling ebsnvme-id.exe: %v", err) + } + // ebsnvme-id.exe will output a list of disks in this format: + // ``` + // Disk Number: 1 + // Volume ID: vol-06d0909eb358b05f9 + // Device Name: /dev/xvdch + // ``` + // Don't try to match devicePath against "Device Name" not only because volume ID is sufficient, + // but because devicePath may change between Linux & Windows formats between WaitForAttach calls. + // The first attach and mount, WaitForAttach gets devicePath as the Linux format /dev/xvdch. Then + // WaitForAttach returns the disk number as the "right" devicePath and that is persisted to ASW. + // In subsequent mounts of the same disk, WaitForAttach gets devicePath as the Windows format it + // returned the first time. + diskRe := regexp.MustCompile( + `Disk Number: (\d+)\s*` + + `Volume ID: ` + volumeID + `\s*`) + matches := diskRe.FindStringSubmatch(string(output)) + if len(matches) != 2 { + return "", fmt.Errorf("disk not found in ebsnvme-id.exe output: %q", string(output)) + } + return matches[1], nil +} diff --git a/pkg/volume/awsebs/aws_ebs.go b/pkg/volume/awsebs/aws_ebs.go index fb0b3ebf6c7..da43520c247 100644 --- a/pkg/volume/awsebs/aws_ebs.go +++ b/pkg/volume/awsebs/aws_ebs.go @@ -22,6 +22,7 @@ import ( "os" "path/filepath" "regexp" + "runtime" "strconv" "strings" @@ -401,8 +402,15 @@ func (b *awsElasticBlockStoreMounter) SetUpAt(dir string, mounterArgs volume.Mou globalPDPath := makeGlobalPDPath(b.plugin.host, b.volumeID) - if err := os.MkdirAll(dir, 0750); err != nil { - return err + if runtime.GOOS != "windows" { + // On Windows, Mount will create the parent of dir and mklink (create a symbolic link) at dir later, so don't create a + // directory at dir now. Otherwise mklink will error: "Cannot create a file when that file already exists". + // Instead, do nothing. For example when dir is: + // C:\var\lib\kubelet\pods\xxx\volumes\kubernetes.io~aws-ebs\pvc-xxx + // do nothing. Mount will make pvc-xxx a symlink to the global mount path (e.g. C:\var\lib\kubelet\plugins\kubernetes.io\aws-ebs\mounts\aws\us-west-2b\vol-xxx) + if err := os.MkdirAll(dir, 0750); err != nil { + return err + } } // Perform a bind mount to the full path to allow duplicate mounts of the same PD.