1// +build !providerless 2 3/* 4Copyright 2015 The Kubernetes Authors. 5 6Licensed under the Apache License, Version 2.0 (the "License"); 7you may not use this file except in compliance with the License. 8You may obtain a copy of the License at 9 10 http://www.apache.org/licenses/LICENSE-2.0 11 12Unless required by applicable law or agreed to in writing, software 13distributed under the License is distributed on an "AS IS" BASIS, 14WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15See the License for the specific language governing permissions and 16limitations under the License. 17*/ 18 19package cinder 20 21import ( 22 "fmt" 23 "os" 24 "path/filepath" 25 "testing" 26 "time" 27 28 v1 "k8s.io/api/core/v1" 29 "k8s.io/apimachinery/pkg/types" 30 utiltesting "k8s.io/client-go/util/testing" 31 "k8s.io/mount-utils" 32 33 "k8s.io/kubernetes/pkg/volume" 34 volumetest "k8s.io/kubernetes/pkg/volume/testing" 35 "k8s.io/kubernetes/pkg/volume/util" 36 "k8s.io/legacy-cloud-providers/openstack" 37) 38 39func TestCanSupport(t *testing.T) { 40 tmpDir, err := utiltesting.MkTmpdir("cinderTest") 41 if err != nil { 42 t.Fatalf("can't make a temp dir: %v", err) 43 } 44 defer os.RemoveAll(tmpDir) 45 plugMgr := volume.VolumePluginMgr{} 46 plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeKubeletVolumeHost(t, tmpDir, nil, nil)) 47 48 plug, err := plugMgr.FindPluginByName("kubernetes.io/cinder") 49 if err != nil { 50 t.Fatal("Can't find the plugin by name") 51 } 52 if plug.GetPluginName() != "kubernetes.io/cinder" { 53 t.Errorf("Wrong name: %s", plug.GetPluginName()) 54 } 55 if !plug.CanSupport(&volume.Spec{Volume: &v1.Volume{VolumeSource: v1.VolumeSource{Cinder: &v1.CinderVolumeSource{}}}}) { 56 t.Errorf("Expected true") 57 } 58 59 if !plug.CanSupport(&volume.Spec{PersistentVolume: &v1.PersistentVolume{Spec: v1.PersistentVolumeSpec{PersistentVolumeSource: v1.PersistentVolumeSource{Cinder: &v1.CinderPersistentVolumeSource{}}}}}) { 60 t.Errorf("Expected true") 61 } 62} 63 64type fakePDManager struct { 65 // How long should AttachDisk/DetachDisk take - we need slower AttachDisk in a test. 66 attachDetachDuration time.Duration 67} 68 69func getFakeDeviceName(host volume.VolumeHost, pdName string) string { 70 return filepath.Join(host.GetPluginDir(cinderVolumePluginName), "device", pdName) 71} 72 73// Real Cinder AttachDisk attaches a cinder volume. If it is not yet mounted, 74// it mounts it to globalPDPath. 75// We create a dummy directory (="device") and bind-mount it to globalPDPath 76func (fake *fakePDManager) AttachDisk(b *cinderVolumeMounter, globalPDPath string) error { 77 globalPath := makeGlobalPDName(b.plugin.host, b.pdName) 78 fakeDeviceName := getFakeDeviceName(b.plugin.host, b.pdName) 79 err := os.MkdirAll(fakeDeviceName, 0750) 80 if err != nil { 81 return err 82 } 83 // Attaching a Cinder volume can be slow... 84 time.Sleep(fake.attachDetachDuration) 85 86 // The volume is "attached", bind-mount it if it's not mounted yet. 87 notmnt, err := b.mounter.IsLikelyNotMountPoint(globalPath) 88 if err != nil { 89 if os.IsNotExist(err) { 90 if err := os.MkdirAll(globalPath, 0750); err != nil { 91 return err 92 } 93 notmnt = true 94 } else { 95 return err 96 } 97 } 98 if notmnt { 99 err = b.mounter.MountSensitiveWithoutSystemd(fakeDeviceName, globalPath, "", []string{"bind"}, nil) 100 if err != nil { 101 return err 102 } 103 } 104 return nil 105} 106 107func (fake *fakePDManager) DetachDisk(c *cinderVolumeUnmounter) error { 108 globalPath := makeGlobalPDName(c.plugin.host, c.pdName) 109 fakeDeviceName := getFakeDeviceName(c.plugin.host, c.pdName) 110 // unmount the bind-mount - should be fast 111 err := c.mounter.Unmount(globalPath) 112 if err != nil { 113 return err 114 } 115 116 // "Detach" the fake "device" 117 err = os.RemoveAll(fakeDeviceName) 118 if err != nil { 119 return err 120 } 121 return nil 122} 123 124func (fake *fakePDManager) CreateVolume(c *cinderVolumeProvisioner, node *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (volumeID string, volumeSizeGB int, labels map[string]string, fstype string, err error) { 125 labels = make(map[string]string) 126 labels[v1.LabelTopologyZone] = "nova" 127 return "test-volume-name", 1, labels, "", nil 128} 129 130func (fake *fakePDManager) DeleteVolume(cd *cinderVolumeDeleter) error { 131 if cd.pdName != "test-volume-name" { 132 return fmt.Errorf("Deleter got unexpected volume name: %s", cd.pdName) 133 } 134 return nil 135} 136 137func TestPlugin(t *testing.T) { 138 tmpDir, err := utiltesting.MkTmpdir("cinderTest") 139 if err != nil { 140 t.Fatalf("can't make a temp dir: %v", err) 141 } 142 defer os.RemoveAll(tmpDir) 143 plugMgr := volume.VolumePluginMgr{} 144 plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeKubeletVolumeHost(t, tmpDir, nil, nil)) 145 146 plug, err := plugMgr.FindPluginByName("kubernetes.io/cinder") 147 if err != nil { 148 t.Errorf("Can't find the plugin by name") 149 } 150 spec := &v1.Volume{ 151 Name: "vol1", 152 VolumeSource: v1.VolumeSource{ 153 Cinder: &v1.CinderVolumeSource{ 154 VolumeID: "pd", 155 FSType: "ext4", 156 }, 157 }, 158 } 159 mounter, err := plug.(*cinderPlugin).newMounterInternal(volume.NewSpecFromVolume(spec), types.UID("poduid"), &fakePDManager{0}, mount.NewFakeMounter(nil)) 160 if err != nil { 161 t.Errorf("Failed to make a new Mounter: %v", err) 162 } 163 if mounter == nil { 164 t.Errorf("Got a nil Mounter") 165 } 166 volPath := filepath.Join(tmpDir, "pods/poduid/volumes/kubernetes.io~cinder/vol1") 167 path := mounter.GetPath() 168 if path != volPath { 169 t.Errorf("Got unexpected path: %s", path) 170 } 171 172 if err := mounter.SetUp(volume.MounterArgs{}); err != nil { 173 t.Errorf("Expected success, got: %v", err) 174 } 175 if _, err := os.Stat(path); err != nil { 176 if os.IsNotExist(err) { 177 t.Errorf("SetUp() failed, volume path not created: %s", path) 178 } else { 179 t.Errorf("SetUp() failed: %v", err) 180 } 181 } 182 183 unmounter, err := plug.(*cinderPlugin).newUnmounterInternal("vol1", types.UID("poduid"), &fakePDManager{0}, mount.NewFakeMounter(nil)) 184 if err != nil { 185 t.Errorf("Failed to make a new Unmounter: %v", err) 186 } 187 if unmounter == nil { 188 t.Errorf("Got a nil Unmounter") 189 } 190 191 if err := unmounter.TearDown(); err != nil { 192 t.Errorf("Expected success, got: %v", err) 193 } 194 if _, err := os.Stat(path); err == nil { 195 t.Errorf("TearDown() failed, volume path still exists: %s", path) 196 } else if !os.IsNotExist(err) { 197 t.Errorf("TearDown() failed: %v", err) 198 } 199 200 // Test Provisioner 201 options := volume.VolumeOptions{ 202 PVC: volumetest.CreateTestPVC("100Mi", []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}), 203 PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete, 204 } 205 provisioner, err := plug.(*cinderPlugin).newProvisionerInternal(options, &fakePDManager{0}) 206 if err != nil { 207 t.Errorf("ProvisionerInternal() failed: %v", err) 208 } 209 persistentSpec, err := provisioner.Provision(nil, nil) 210 if err != nil { 211 t.Errorf("Provision() failed: %v", err) 212 } 213 214 if persistentSpec.Spec.PersistentVolumeSource.Cinder.VolumeID != "test-volume-name" { 215 t.Errorf("Provision() returned unexpected volume ID: %s", persistentSpec.Spec.PersistentVolumeSource.Cinder.VolumeID) 216 } 217 cap := persistentSpec.Spec.Capacity[v1.ResourceStorage] 218 size := cap.Value() 219 if size != 1024*1024*1024 { 220 t.Errorf("Provision() returned unexpected volume size: %v", size) 221 } 222 223 // check nodeaffinity members 224 if persistentSpec.Spec.NodeAffinity == nil { 225 t.Errorf("Provision() returned unexpected nil NodeAffinity") 226 } 227 228 if persistentSpec.Spec.NodeAffinity.Required == nil { 229 t.Errorf("Provision() returned unexpected nil NodeAffinity.Required") 230 } 231 232 n := len(persistentSpec.Spec.NodeAffinity.Required.NodeSelectorTerms) 233 if n != 1 { 234 t.Errorf("Provision() returned unexpected number of NodeSelectorTerms %d. Expected %d", n, 1) 235 } 236 237 n = len(persistentSpec.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions) 238 if n != 1 { 239 t.Errorf("Provision() returned unexpected number of MatchExpressions %d. Expected %d", n, 1) 240 } 241 242 req := persistentSpec.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions[0] 243 244 if req.Key != v1.LabelTopologyZone { 245 t.Errorf("Provision() returned unexpected requirement key in NodeAffinity %v", req.Key) 246 } 247 248 if req.Operator != v1.NodeSelectorOpIn { 249 t.Errorf("Provision() returned unexpected requirement operator in NodeAffinity %v", req.Operator) 250 } 251 252 if len(req.Values) != 1 || req.Values[0] != "nova" { 253 t.Errorf("Provision() returned unexpected requirement value in NodeAffinity %v", req.Values) 254 } 255 256 // Test Deleter 257 volSpec := &volume.Spec{ 258 PersistentVolume: persistentSpec, 259 } 260 deleter, err := plug.(*cinderPlugin).newDeleterInternal(volSpec, &fakePDManager{0}) 261 if err != nil { 262 t.Errorf("DeleterInternal() failed: %v", err) 263 } 264 err = deleter.Delete() 265 if err != nil { 266 t.Errorf("Deleter() failed: %v", err) 267 } 268} 269 270func TestGetVolumeLimit(t *testing.T) { 271 tmpDir, err := utiltesting.MkTmpdir("cinderTest") 272 if err != nil { 273 t.Fatalf("can't make a temp dir: %v", err) 274 } 275 276 cloud, err := getOpenstackCloudProvider() 277 if err != nil { 278 t.Fatalf("can not instantiate openstack cloudprovider : %v", err) 279 } 280 281 defer os.RemoveAll(tmpDir) 282 plugMgr := volume.VolumePluginMgr{} 283 volumeHost := volumetest.NewFakeKubeletVolumeHostWithCloudProvider(t, tmpDir, nil, nil, cloud) 284 plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumeHost) 285 286 plug, err := plugMgr.FindPluginByName("kubernetes.io/cinder") 287 if err != nil { 288 t.Fatalf("Can't find the plugin by name") 289 } 290 attachablePlugin, ok := plug.(volume.VolumePluginWithAttachLimits) 291 if !ok { 292 t.Fatalf("plugin %s is not of attachable type", plug.GetPluginName()) 293 } 294 295 limits, err := attachablePlugin.GetVolumeLimits() 296 if err != nil { 297 t.Errorf("error fetching limits : %v", err) 298 } 299 if len(limits) == 0 { 300 t.Fatalf("expecting limit from openstack got none") 301 } 302 limit, _ := limits[util.CinderVolumeLimitKey] 303 if limit != 10 { 304 t.Fatalf("expected volume limit to be 10 got %d", limit) 305 } 306} 307 308func getOpenstackCloudProvider() (*openstack.OpenStack, error) { 309 cfg := getOpenstackConfig() 310 return openstack.NewFakeOpenStackCloud(cfg) 311} 312 313func getOpenstackConfig() openstack.Config { 314 cfg := openstack.Config{ 315 Global: struct { 316 AuthURL string `gcfg:"auth-url"` 317 Username string 318 UserID string `gcfg:"user-id"` 319 Password string `datapolicy:"password"` 320 TenantID string `gcfg:"tenant-id"` 321 TenantName string `gcfg:"tenant-name"` 322 TrustID string `gcfg:"trust-id"` 323 DomainID string `gcfg:"domain-id"` 324 DomainName string `gcfg:"domain-name"` 325 Region string 326 CAFile string `gcfg:"ca-file"` 327 SecretName string `gcfg:"secret-name"` 328 SecretNamespace string `gcfg:"secret-namespace"` 329 KubeconfigPath string `gcfg:"kubeconfig-path"` 330 }{ 331 Username: "user", 332 Password: "pass", 333 TenantID: "foobar", 334 DomainID: "2a73b8f597c04551a0fdc8e95544be8a", 335 DomainName: "local", 336 AuthURL: "http://auth.url", 337 UserID: "user", 338 }, 339 BlockStorage: openstack.BlockStorageOpts{ 340 NodeVolumeAttachLimit: 10, 341 }, 342 } 343 return cfg 344} 345 346func TestUnsupportedVolumeHost(t *testing.T) { 347 tmpDir, err := utiltesting.MkTmpdir("cinderTest") 348 if err != nil { 349 t.Fatalf("can't make a temp dir: %v", err) 350 } 351 defer os.RemoveAll(tmpDir) 352 plugMgr := volume.VolumePluginMgr{} 353 plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(t, tmpDir, nil, nil)) 354 355 plug, err := plugMgr.FindPluginByName("kubernetes.io/cinder") 356 if err != nil { 357 t.Fatal("Can't find the plugin by name") 358 } 359 360 _, err = plug.ConstructVolumeSpec("", "") 361 if err == nil { 362 t.Errorf("Expected failure constructing volume spec with unsupported VolumeHost") 363 } 364} 365