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