Skip to content
This repository was archived by the owner on Mar 22, 2018. It is now read-only.

Commit d06c0d1

Browse files
openstack: fetch volume path from metadata service
The OpenStack cloud provider retrieves mounted Cinder volume paths by looking in /dev/disk/by-id, expecting the disk serial IDs (e.g. SCSI ID) to include the volume ID. The issue is that not all hypervisors are able to expose this. For example, Hyper-V will just preserve the original Cinder volume lun SCSI ID (without setting the volume id). For this reason, disk path lookups will fail. In order to be able to leverage Hyper-V based OpenStack providers, as a fallback, we're querying the metadata service, searching for disk device metadata. Note that starting with Nova Queens, the Hyper-V driver always provides disk address information through the instance metadata.
1 parent 3cb72bd commit d06c0d1

4 files changed

Lines changed: 139 additions & 18 deletions

File tree

pkg/cloudprovider/providers/openstack/metadata.go

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,33 +33,45 @@ import (
3333
)
3434

3535
const (
36-
// metadataUrl is URL to OpenStack metadata server. It's hardcoded IPv4
37-
// link-local address as documented in "OpenStack Cloud Administrator Guide",
38-
// chapter Compute - Networking with nova-network.
36+
// metadataUrlTemplate allows building an OpenStack Metadata service URL.
37+
// It's a hardcoded IPv4 link-local address as documented in "OpenStack Cloud
38+
// Administrator Guide", chapter Compute - Networking with nova-network.
3939
// https://docs.openstack.org/admin-guide/compute-networking-nova.html#metadata-service
40-
metadataUrl = "http://169.254.169.254/openstack/2012-08-10/meta_data.json"
40+
defaultMetadataVersion = "2012-08-10"
41+
metadataUrlTemplate = "http://169.254.169.254/openstack/%s/meta_data.json"
4142

4243
// metadataID is used as an identifier on the metadata search order configuration.
4344
metadataID = "metadataService"
4445

4546
// Config drive is defined as an iso9660 or vfat (deprecated) drive
4647
// with the "config-2" label.
4748
// http://docs.openstack.org/user-guide/cli-config-drive.html
48-
configDriveLabel = "config-2"
49-
configDrivePath = "openstack/2012-08-10/meta_data.json"
49+
configDriveLabel = "config-2"
50+
configDrivePathTemplate = "openstack/%s/meta_data.json"
5051

5152
// configDriveID is used as an identifier on the metadata search order configuration.
5253
configDriveID = "configDrive"
5354
)
5455

5556
var ErrBadMetadata = errors.New("invalid OpenStack metadata, got empty uuid")
5657

58+
// There are multiple device types. To keep it simple, we're using a single structure
59+
// for all device metadata types.
60+
type DeviceMetadata struct {
61+
Type string `json:"type"`
62+
Bus string `json:"bus,omitempty"`
63+
Serial string `json:"serial,omitempty"`
64+
Address string `json:"address,omitempty"`
65+
// .. and other fields.
66+
}
67+
5768
// Assumes the "2012-08-10" meta_data.json format.
5869
// See http://docs.openstack.org/user-guide/cli_config_drive.html
5970
type Metadata struct {
60-
Uuid string `json:"uuid"`
61-
Name string `json:"name"`
62-
AvailabilityZone string `json:"availability_zone"`
71+
Uuid string `json:"uuid"`
72+
Name string `json:"name"`
73+
AvailabilityZone string `json:"availability_zone"`
74+
Devices []DeviceMetadata `json:"devices,omitempty"`
6375
// .. and other fields we don't care about. Expand as necessary.
6476
}
6577

@@ -79,7 +91,15 @@ func parseMetadata(r io.Reader) (*Metadata, error) {
7991
return &metadata, nil
8092
}
8193

82-
func getMetadataFromConfigDrive() (*Metadata, error) {
94+
func getMetadataUrl(metadataVersion string) string {
95+
return fmt.Sprintf(metadataUrlTemplate, metadataVersion)
96+
}
97+
98+
func getConfigDrivePath(metadataVersion string) string {
99+
return fmt.Sprintf(configDrivePathTemplate, metadataVersion)
100+
}
101+
102+
func getMetadataFromConfigDrive(metadataVersion string) (*Metadata, error) {
83103
// Try to read instance UUID from config drive.
84104
dev := "/dev/disk/by-label/" + configDriveLabel
85105
if _, err := os.Stat(dev); os.IsNotExist(err) {
@@ -114,6 +134,7 @@ func getMetadataFromConfigDrive() (*Metadata, error) {
114134

115135
glog.V(4).Infof("Configdrive mounted on %s", mntdir)
116136

137+
configDrivePath := getConfigDrivePath(metadataVersion)
117138
f, err := os.Open(
118139
filepath.Join(mntdir, configDrivePath))
119140
if err != nil {
@@ -124,8 +145,9 @@ func getMetadataFromConfigDrive() (*Metadata, error) {
124145
return parseMetadata(f)
125146
}
126147

127-
func getMetadataFromMetadataService() (*Metadata, error) {
148+
func getMetadataFromMetadataService(metadataVersion string) (*Metadata, error) {
128149
// Try to get JSON from metadata server.
150+
metadataUrl := getMetadataUrl(metadataVersion)
129151
glog.V(4).Infof("Attempting to fetch metadata from %s", metadataUrl)
130152
resp, err := http.Get(metadataUrl)
131153
if err != nil {
@@ -154,9 +176,9 @@ func getMetadata(order string) (*Metadata, error) {
154176
id = strings.TrimSpace(id)
155177
switch id {
156178
case configDriveID:
157-
md, err = getMetadataFromConfigDrive()
179+
md, err = getMetadataFromConfigDrive(defaultMetadataVersion)
158180
case metadataID:
159-
md, err = getMetadataFromMetadataService()
181+
md, err = getMetadataFromMetadataService(defaultMetadataVersion)
160182
default:
161183
err = fmt.Errorf("%s is not a valid metadata search order option. Supported options are %s and %s", id, configDriveID, metadataID)
162184
}

pkg/cloudprovider/providers/openstack/metadata_test.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,16 @@ func TestParseMetadata(t *testing.T) {
6464
"public_keys": {
6565
"mykey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDBqUfVvCSez0/Wfpd8dLLgZXV9GtXQ7hnMN+Z0OWQUyebVEHey1CXuin0uY1cAJMhUq8j98SiW+cU0sU4J3x5l2+xi1bodDm1BtFWVeLIOQINpfV1n8fKjHB+ynPpe1F6tMDvrFGUlJs44t30BrujMXBe8Rq44cCk6wqyjATA3rQ== Generated by Nova\n"
6666
},
67-
"uuid": "83679162-1378-4288-a2d4-70e13ec132aa"
67+
"uuid": "83679162-1378-4288-a2d4-70e13ec132aa",
68+
"devices": [
69+
{
70+
"bus": "scsi",
71+
"serial": "6df1888b-f373-41cf-b960-3786e60a28ef",
72+
"tags": ["fake_tag"],
73+
"type": "disk",
74+
"address": "0:0:0:0"
75+
}
76+
]
6877
}
6978
`)
7079
md, err := parseMetadata(data)
@@ -83,4 +92,20 @@ func TestParseMetadata(t *testing.T) {
8392
if md.AvailabilityZone != "nova" {
8493
t.Errorf("incorrect az: %s", md.AvailabilityZone)
8594
}
95+
96+
if len(md.Devices) != 1 {
97+
t.Errorf("expecting to find 1 device, found %d", len(md.Devices))
98+
}
99+
100+
if md.Devices[0].Bus != "scsi" {
101+
t.Errorf("incorrect disk bus: %s", md.Devices[0].Bus)
102+
}
103+
104+
if md.Devices[0].Address != "0:0:0:0" {
105+
t.Errorf("incorrect disk address: %s", md.Devices[0].Address)
106+
}
107+
108+
if md.Devices[0].Type != "disk" {
109+
t.Errorf("incorrect device type: %s", md.Devices[0].Type)
110+
}
86111
}

pkg/cloudprovider/providers/openstack/openstack_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"os"
2222
"reflect"
23+
"regexp"
2324
"sort"
2425
"strings"
2526
"testing"
@@ -498,6 +499,8 @@ func TestZones(t *testing.T) {
498499
}
499500
}
500501

502+
var diskPathRegexp = regexp.MustCompile("/dev/disk/(?:by-id|by-path)/")
503+
501504
func TestVolumes(t *testing.T) {
502505
cfg, ok := configFromEnv()
503506
if !ok {
@@ -534,7 +537,7 @@ func TestVolumes(t *testing.T) {
534537
WaitForVolumeStatus(t, os, vol, volumeInUseStatus)
535538

536539
devicePath := os.GetDevicePath(diskId)
537-
if !strings.HasPrefix(devicePath, "/dev/disk/by-id/") {
540+
if diskPathRegexp.FindString(devicePath) == "" {
538541
t.Fatalf("GetDevicePath returned and unexpected path for Cinder volume %s, returned %s", vol, devicePath)
539542
}
540543
t.Logf("Volume (%s) found at path: %s\n", vol, devicePath)

pkg/cloudprovider/providers/openstack/openstack_volumes.go

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"io/ioutil"
2222
"path"
23+
"path/filepath"
2324
"strings"
2425
"time"
2526

@@ -78,6 +79,11 @@ const (
7879
VolumeInUseStatus = "in-use"
7980
VolumeDeletedStatus = "deleted"
8081
VolumeErrorStatus = "error"
82+
83+
// On some environments, we need to query the metadata service in order
84+
// to locate disks. We'll use the Newton version, which includes device
85+
// metadata.
86+
NewtonMetadataVersion = "2016-06-30"
8187
)
8288

8389
func (volumes *VolumesV1) createVolume(opts VolumeCreateOpts) (string, string, error) {
@@ -305,8 +311,9 @@ func (os *OpenStack) CreateVolume(name string, size int, vtype, availability str
305311
}
306312

307313
// GetDevicePath returns the path of an attached block storage volume, specified by its id.
308-
func (os *OpenStack) GetDevicePath(volumeID string) string {
309-
// Build a list of candidate device paths
314+
func (os *OpenStack) GetDevicePathBySerialId(volumeID string) string {
315+
// Build a list of candidate device paths.
316+
// Certain Nova drivers will set the disk serial ID, including the Cinder volume id.
310317
candidateDeviceNodes := []string{
311318
// KVM
312319
fmt.Sprintf("virtio-%s", volumeID[:20]),
@@ -327,10 +334,74 @@ func (os *OpenStack) GetDevicePath(volumeID string) string {
327334
}
328335
}
329336

330-
glog.Warningf("Failed to find device for the volumeID: %q\n", volumeID)
337+
glog.V(4).Infof("Failed to find device for the volumeID: %q by serial ID", volumeID)
331338
return ""
332339
}
333340

341+
func (os *OpenStack) GetDevicePathFromInstanceMetadata(volumeID string) string {
342+
// Nova Hyper-V hosts cannot override disk SCSI IDs. In order to locate
343+
// volumes, we're querying the metadata service. Note that the Hyper-V
344+
// driver will include device metadata for untagged volumes as well.
345+
//
346+
// We're avoiding using cached metadata (or the configdrive),
347+
// relying on the metadata service.
348+
instanceMetadata, err := getMetadataFromMetadataService(
349+
NewtonMetadataVersion)
350+
351+
if err != nil {
352+
glog.V(4).Infof(
353+
"Could not retrieve instance metadata. Error: %v", err)
354+
return ""
355+
}
356+
357+
for _, device := range instanceMetadata.Devices {
358+
if device.Type == "disk" && device.Serial == volumeID {
359+
glog.V(4).Infof(
360+
"Found disk metadata for volumeID %q. Bus: %q, Address: %q",
361+
volumeID, device.Bus, device.Address)
362+
363+
diskPattern := fmt.Sprintf(
364+
"/dev/disk/by-path/*-%s-%s",
365+
device.Bus, device.Address)
366+
diskPaths, err := filepath.Glob(diskPattern)
367+
if err != nil {
368+
glog.Errorf(
369+
"could not retrieve disk path for volumeID: %q. Error filepath.Glob(%q): %v",
370+
volumeID, diskPattern, err)
371+
return ""
372+
}
373+
374+
if len(diskPaths) == 1 {
375+
return diskPaths[0]
376+
}
377+
378+
glog.Errorf(
379+
"expecting to find one disk path for volumeID %q, found %d: %v",
380+
volumeID, len(diskPaths), diskPaths)
381+
return ""
382+
}
383+
}
384+
385+
glog.V(4).Infof(
386+
"Could not retrieve device metadata for volumeID: %q", volumeID)
387+
return ""
388+
}
389+
390+
// GetDevicePath returns the path of an attached block storage volume, specified by its id.
391+
func (os *OpenStack) GetDevicePath(volumeID string) string {
392+
devicePath := os.GetDevicePathBySerialId(volumeID)
393+
394+
if devicePath == "" {
395+
devicePath = os.GetDevicePathFromInstanceMetadata(volumeID)
396+
}
397+
398+
if devicePath == "" {
399+
glog.Warningf("Failed to find device for the volumeID: %q", volumeID)
400+
}
401+
402+
return devicePath
403+
}
404+
334405
func (os *OpenStack) DeleteVolume(volumeID string) error {
335406
used, err := os.diskIsUsed(volumeID)
336407
if err != nil {

0 commit comments

Comments
 (0)