diff --git a/Makefile b/Makefile index 9d32da129d2acd3a1cd96463ff2d8ae9a7d72399..982da4d28e1a26ede3ac242200c3069ddfa99a32 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ ifeq ($(VERSION),) VERSION = latest endif -clean: clean-aws/efs clean-ceph/cephfs clean-flex clean-gluster/block clean-local-volume/provisioner clean-local-volume/bootstrapper clean-nfs-client clean-nfs +clean: clean-aws/efs clean-ceph/cephfs clean-ceph/rbd clean-flex clean-gluster/block clean-local-volume/provisioner clean-local-volume/bootstrapper clean-nfs-client clean-nfs .PHONY: clean test: test-aws/efs test-local-volume/provisioner test-nfs @@ -57,6 +57,18 @@ clean-ceph/cephfs: rm -f cephfs-provisioner .PHONY: clean-ceph/cephfs +ceph/rbd: + cd ceph/rbd; \ + go build -o rbd-provisioner cmd/rbd-provisioner/main.go; \ + docker build -t $(REGISTRY)rbd-provisioner:latest . + docker tag $(REGISTRY)rbd-provisioner:latest $(REGISTRY)rbd-provisioner:$(VERSION) +.PHONY: ceph/rbd + +clean-ceph/rbd: + cd ceph/rbd; \ + rm -f rbd-provisioner +.PHONY: clean-ceph/rbd + flex: cd flex; \ make container @@ -129,6 +141,11 @@ push-cephfs-provisioner: ceph/cephfs docker push $(REGISTRY)cephfs-provisioner:latest .PHONY: push-cephfs-provisioner +push-rbd-provisioner: ceph/rbd + docker push $(REGISTRY)rbd-provisioner:$(VERSION) + docker push $(REGISTRY)rbd-provisioner:latest +.PHONY: push-nfs-client-provisioner + push-efs-provisioner: cd aws/efs; \ make push diff --git a/ceph/rbd/Dockerfile b/ceph/rbd/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f8cbc39a7177c3fad36438d2923cfb43d048e222 --- /dev/null +++ b/ceph/rbd/Dockerfile @@ -0,0 +1,20 @@ +# Copyright 2017 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. + +FROM centos:7 +RUN rpm -Uvh https://download.ceph.com/rpm-jewel/el7/noarch/ceph-release-1-1.el7.noarch.rpm +RUN yum install -y epel-release +RUN yum install -y ceph-common +ADD rbd-provisioner /usr/local/bin/rbd-provisioner +ENTRYPOINT ["/usr/local/bin/rbd-provisioner"] diff --git a/ceph/rbd/OWNERS b/ceph/rbd/OWNERS new file mode 100644 index 0000000000000000000000000000000000000000..ef8694ba52dbf89faf831af1d2ce5569803c6db9 --- /dev/null +++ b/ceph/rbd/OWNERS @@ -0,0 +1,3 @@ +assignees: + - rootfs + - cofyc diff --git a/ceph/rbd/README.md b/ceph/rbd/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e43e0ae68fe6bda3b812bacc3a6e7707348b93dd --- /dev/null +++ b/ceph/rbd/README.md @@ -0,0 +1,74 @@ +# RBD Volume Provisioner for Kubernetes 1.5+ + +`rbd-provisioner` is an out-of-tree dynamic provisioner for Kubernetes 1.5+. +You can use it quickly & easily deploy ceph RBD storage that works almost +anywhere. + +It works just like in-tree dynamic provisioner. For more information on how +dynamic provisioning works, see [the docs](http://kubernetes.io/docs/user-guide/persistent-volumes/) +or [this blog post](http://blog.kubernetes.io/2016/10/dynamic-provisioning-and-storage-in-kubernetes.html). + +## Test instruction + +* Build rbd-provisioner and container image + +```bash +go build -o rbd-provisioner cmd/rbd-provisioner/main.go +docker build -t rbd-provisioner . +``` + +* Start Kubernetes local cluster + +* Create a Ceph admin secret + +```bash +ceph auth get client.admin 2>&1 |grep "key = " |awk '{print $3'} |xargs echo -n > /tmp/secret +kubectl create secret generic ceph-admin-secret --from-file=/tmp/secret --namespace=kube-system +``` + +* Create a Ceph pool and a user secret + +```bash +ceph osd pool create kube 8 8 +ceph auth add client.kube mon 'allow r' osd 'allow rwx pool=kube' +ceph auth get client.kube 2>&1 |grep "key = " |awk '{print $3'} |xargs echo -n > /tmp/secret +kubectl create secret generic ceph-secret --from-file=/tmp/secret --namespace=default +``` + +* Start RBD provisioner + +The following example uses `rbd-provisioner-1` as the identity for the instance and assumes kubeconfig is at `/root/.kube`. The identity should remain the same if the provisioner restarts. If there are multiple provisioners, each should have a different identity. + +```bash +docker run -ti -v /root/.kube:/kube -v /var/run/kubernetes:/var/run/kubernetes --privileged --net=host rbd-provisioner /usr/local/bin/rbd-provisioner -master=http://127.0.0.1:8080 -kubeconfig=/kube/config -id=rbd-provisioner-1 +``` + +Alternatively, start a deployment: + +```bash +kubectl create -f deployment.yaml +``` + +* Create a RBD Storage Class + +Replace Ceph monitor's IP in [class.yaml](class.yaml) with your own and create storage class: + +```bash +kubectl create -f class.yaml +``` + +* Create a claim + +```bash +kubectl create -f claim.yaml +``` + +* Create a Pod using the claim + +```bash +kubectl create -f test-pod.yaml +``` + +# Acknowledgements + +- This provisioner is extracted from [Kubernetes core](https://github.com/kubernetes/kubernetes) with some modifications for this project. diff --git a/ceph/rbd/claim.yaml b/ceph/rbd/claim.yaml new file mode 100644 index 0000000000000000000000000000000000000000..357fd4ebdd081c24a639a06bc1e1ab0e09a898e8 --- /dev/null +++ b/ceph/rbd/claim.yaml @@ -0,0 +1,11 @@ +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: claim1 +spec: + accessModes: + - ReadWriteOnce + storageClassName: rbd + resources: + requests: + storage: 1Gi diff --git a/ceph/rbd/class.yaml b/ceph/rbd/class.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2323c2aca9e8cd5bc1b6b8c955481096e81a561c --- /dev/null +++ b/ceph/rbd/class.yaml @@ -0,0 +1,15 @@ +kind: StorageClass +apiVersion: storage.k8s.io/v1 +metadata: + name: rbd +provisioner: ceph.com/rbd +parameters: + monitors: 172.16.118.132:6789 + pool: kube + adminId: admin + adminSecretNamespace: kube-system + adminSecretName: ceph-admin-secret + userId: kube + userSecretName: ceph-secret + imageFormat: "2" + imageFeatures: layering diff --git a/ceph/rbd/cmd/rbd-provisioner/main.go b/ceph/rbd/cmd/rbd-provisioner/main.go new file mode 100644 index 0000000000000000000000000000000000000000..c96ae85dc8221734b975147a0851833b4af31ae3 --- /dev/null +++ b/ceph/rbd/cmd/rbd-provisioner/main.go @@ -0,0 +1,83 @@ +/* +Copyright 2017 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 main + +import ( + "flag" + + "github.com/golang/glog" + "github.com/kubernetes-incubator/external-storage/ceph/rbd/pkg/provision" + "github.com/kubernetes-incubator/external-storage/lib/controller" + "k8s.io/apimachinery/pkg/util/uuid" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +var ( + master = flag.String("master", "", "Master URL") + kubeconfig = flag.String("kubeconfig", "", "Absolute path to the kubeconfig") + id = flag.String("id", "", "Unique provisioner identity") +) + +func main() { + flag.Parse() + flag.Set("logtostderr", "true") + + var config *rest.Config + var err error + if *master != "" || *kubeconfig != "" { + config, err = clientcmd.BuildConfigFromFlags(*master, *kubeconfig) + } else { + config, err = rest.InClusterConfig() + } + prID := string(uuid.NewUUID()) + if *id != "" { + prID = *id + } + if err != nil { + glog.Fatalf("Failed to create config: %v", err) + } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + glog.Fatalf("Failed to create client: %v", err) + } + + // The controller needs to know what the server version is because out-of-tree + // provisioners aren't officially supported until 1.5 + serverVersion, err := clientset.Discovery().ServerVersion() + if err != nil { + glog.Fatalf("Error getting server version: %v", err) + } + + // Create the provisioner: it implements the Provisioner interface expected by + // the controller + glog.Infof("Creating RBD provisioner with identity: %s", prID) + rbdProvisioner := provision.NewRBDProvisioner(clientset, prID) + + // Start the provision controller which will dynamically provision rbd + // PVs + pc := controller.NewProvisionController( + clientset, + provision.ProvisionerName, + rbdProvisioner, + serverVersion.GitVersion, + ) + + pc.Run(wait.NeverStop) +} diff --git a/ceph/rbd/deployment.yaml b/ceph/rbd/deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..07a5707db378b1ef54dfbd149bf544a6cbe75b25 --- /dev/null +++ b/ceph/rbd/deployment.yaml @@ -0,0 +1,16 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: rbd-provisioner +spec: + replicas: 1 + strategy: + type: Recreate + template: + metadata: + labels: + app: rbd-provisioner + spec: + containers: + - name: rbd-provisioner + image: "quay.io/external_storage/rbd-provisioner:latest" diff --git a/ceph/rbd/local-start.sh b/ceph/rbd/local-start.sh new file mode 100755 index 0000000000000000000000000000000000000000..b82fdccc2fb3d72041848ae78cbac065948335b7 --- /dev/null +++ b/ceph/rbd/local-start.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Copyright 2017 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. + +PATH=${PATH}:`pwd` + +if [[ "$KUBECONFIG" == "" ]]; then + KUBECONFIG=/root/.kube/config +fi + +rbd-provisioner -id=rbd-provisioner-1 -master=http://127.0.0.1:8080 -kubeconfig=${KUBECONFIG} -logtostderr diff --git a/ceph/rbd/pkg/provision/helper.go b/ceph/rbd/pkg/provision/helper.go new file mode 100644 index 0000000000000000000000000000000000000000..beaa4ba3e037aee0af6ae3f18cfd35fd79c38713 --- /dev/null +++ b/ceph/rbd/pkg/provision/helper.go @@ -0,0 +1,42 @@ +/* +Copyright 2017 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. +*/ + +// Helper utlities copied from kubernetes/pkg/volume. +// TODO: Merge this into github.com/kubernetes-incubator/external-storage/lib/{util or helper} if possible. + +package provision + +import "k8s.io/client-go/pkg/api/v1" + +// AccessModesContains returns whether the requested mode is contained by modes +func AccessModesContains(modes []v1.PersistentVolumeAccessMode, mode v1.PersistentVolumeAccessMode) bool { + for _, m := range modes { + if m == mode { + return true + } + } + return false +} + +// AccessModesContainedInAll returns whether all of the requested modes are contained by modes +func AccessModesContainedInAll(indexedModes []v1.PersistentVolumeAccessMode, requestedModes []v1.PersistentVolumeAccessMode) bool { + for _, mode := range requestedModes { + if !AccessModesContains(indexedModes, mode) { + return false + } + } + return true +} diff --git a/ceph/rbd/pkg/provision/provision.go b/ceph/rbd/pkg/provision/provision.go new file mode 100644 index 0000000000000000000000000000000000000000..96cd4f8bf29135625f4d10cabfcb906e6281d166 --- /dev/null +++ b/ceph/rbd/pkg/provision/provision.go @@ -0,0 +1,272 @@ +/* +Copyright 2017 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 provision + +import ( + "errors" + "fmt" + "strings" + + "github.com/golang/glog" + "github.com/kubernetes-incubator/external-storage/lib/controller" + "github.com/kubernetes-incubator/external-storage/lib/helper" + "github.com/pborman/uuid" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/pkg/api/v1" +) + +const ( + // ProvisionerName is a unique string to represent this volume provisioner. This value will be + // added in PV annotations under 'pv.kubernetes.io/provisioned-by' key. + ProvisionerName = "ceph.com/rbd" + // Each provisioner have a identify string to distinguish with others. This + // identify string will be added in PV annoations under this key. + provisionerIDAnn = "rbdProvisionerIdentity" + + secretKeyName = "key" // key name used in secret + rbdImageFormat1 = "1" + rbdImageFormat2 = "2" +) + +var ( + supportedFeatures = sets.NewString("layering") +) + +type rbdProvisionOptions struct { + monitors []string + pool string + adminSecret string + adminID string + userID string + userSecretName string + imageFormat string + imageFeatures []string +} + +type rbdProvisioner struct { + // Kubernetes Client. Use to retrieve Ceph admin secret + client kubernetes.Interface + // Identity of this rbdProvisioner, generated. Used to identify "this" + // provisioner's PVs. + identity string + rbdUtil *RBDUtil +} + +// NewRBDProvisioner creates a Provisioner that provisions Ceph RBD PVs backed by Ceph RBD images. +func NewRBDProvisioner(client kubernetes.Interface, id string) controller.Provisioner { + return &rbdProvisioner{ + client: client, + identity: id, + rbdUtil: &RBDUtil{}, + } +} + +var _ controller.Provisioner = &rbdProvisioner{} + +// getAccessModes returns access modes RBD volume supported. +func (p *rbdProvisioner) getAccessModes() []v1.PersistentVolumeAccessMode { + return []v1.PersistentVolumeAccessMode{ + v1.ReadWriteOnce, + v1.ReadOnlyMany, + } +} + +// Provision creates a storage asset and returns a PV object representing it. +func (p *rbdProvisioner) Provision(options controller.VolumeOptions) (*v1.PersistentVolume, error) { + if !AccessModesContainedInAll(p.getAccessModes(), options.PVC.Spec.AccessModes) { + return nil, fmt.Errorf("invalid AccessModes %v: only AccessModes %v are supported", options.PVC.Spec.AccessModes, p.getAccessModes()) + } + if options.PVC.Spec.Selector != nil { + return nil, fmt.Errorf("claim Selector is not supported") + } + opts, err := p.parseParameters(options.Parameters) + if err != nil { + return nil, err + } + // create random image name + image := fmt.Sprintf("kubernetes-dynamic-pvc-%s", uuid.NewUUID()) + rbd, sizeMB, err := p.rbdUtil.CreateImage(image, opts, options) + if err != nil { + glog.Errorf("rbd: create volume failed, err: %v", err) + return nil, err + } + glog.Infof("successfully created rbd image %q", image) + + rbd.SecretRef = new(v1.LocalObjectReference) + rbd.SecretRef.Name = opts.userSecretName + rbd.RadosUser = opts.userID + + pv := &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: options.PVName, + Annotations: map[string]string{ + provisionerIDAnn: p.identity, + }, + }, + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: options.PersistentVolumeReclaimPolicy, + AccessModes: options.PVC.Spec.AccessModes, + Capacity: v1.ResourceList{ + v1.ResourceName(v1.ResourceStorage): resource.MustParse(fmt.Sprintf("%dMi", sizeMB)), + }, + PersistentVolumeSource: v1.PersistentVolumeSource{ + RBD: rbd, + }, + }, + } + // use default access modes if missing + if len(pv.Spec.AccessModes) == 0 { + glog.Warningf("no access modes specified, use default: %v", p.getAccessModes()) + pv.Spec.AccessModes = p.getAccessModes() + } + + return pv, nil +} + +// Delete removes the storage asset that was created by Provision represented +// by the given PV. +func (p *rbdProvisioner) Delete(volume *v1.PersistentVolume) error { + // TODO: Should we check `pv.kubernetes.io/provisioned-by` key too? + ann, ok := volume.Annotations[provisionerIDAnn] + if !ok { + return errors.New("identity annotation not found on PV") + } + if ann != p.identity { + return &controller.IgnoredError{Reason: "identity annotation on PV does not match ours"} + } + + class, err := p.client.StorageV1beta1().StorageClasses().Get(helper.GetPersistentVolumeClass(volume), metav1.GetOptions{}) + if err != nil { + return err + } + opts, err := p.parseParameters(class.Parameters) + if err != nil { + return err + } + image := volume.Spec.PersistentVolumeSource.RBD.RBDImage + return p.rbdUtil.DeleteImage(image, opts) +} + +func (p *rbdProvisioner) parseParameters(parameters map[string]string) (*rbdProvisionOptions, error) { + // options with default values + opts := &rbdProvisionOptions{ + pool: "rbd", + adminID: "admin", + imageFormat: rbdImageFormat1, + } + + var ( + err error + adminSecretName = "" + adminSecretNamespace = "default" + ) + + for k, v := range parameters { + switch strings.ToLower(k) { + case "monitors": + arr := strings.Split(v, ",") + for _, m := range arr { + opts.monitors = append(opts.monitors, m) + } + if len(opts.monitors) < 1 { + return nil, fmt.Errorf("missing Ceph monitors") + } + case "adminid": + if v == "" { + // keep consistent behavior with in-tree rbd provisioner, which use default value if user provides empty string + // TODO: treat empty string invalid value? + v = "admin" + } + opts.adminID = v + case "adminsecretname": + adminSecretName = v + case "adminsecretnamespace": + adminSecretNamespace = v + case "userid": + opts.userID = v + case "pool": + if v == "" { + // keep consistent behavior with in-tree rbd provisioner, which use default value if user provides empty string + // TODO: treat empty string invalid value? + v = "rbd" + } + opts.pool = v + case "usersecretname": + if v == "" { + return nil, fmt.Errorf("missing user secret name") + } + opts.userSecretName = v + case "imageformat": + if v != rbdImageFormat1 && v != rbdImageFormat2 { + return nil, fmt.Errorf("invalid ceph imageformat %s, expecting %s or %s", v, rbdImageFormat1, rbdImageFormat2) + } + opts.imageFormat = v + case "imagefeatures": + arr := strings.Split(v, ",") + for _, f := range arr { + if !supportedFeatures.Has(f) { + return nil, fmt.Errorf("invalid feature %q for %s provisioner, supported features are: %v", f, ProvisionerName, supportedFeatures) + } + opts.imageFeatures = append(opts.imageFeatures, f) + } + default: + return nil, fmt.Errorf("invalid option %q for %s provisioner", k, ProvisionerName) + } + } + + // find adminSecret + var secret string + if adminSecretName == "" { + return nil, fmt.Errorf("missing Ceph admin secret name") + } + if secret, err = p.parsePVSecret(adminSecretNamespace, adminSecretName); err != nil { + return nil, fmt.Errorf("failed to get admin secret from [%q/%q]: %v", adminSecretNamespace, adminSecretName, err) + } + opts.adminSecret = secret + + // set user ID to admin ID if empty + if opts.userID == "" { + opts.userID = opts.adminID + } + + return opts, nil +} + +// parsePVSecret retrives secret value for a given namespace and name. +func (p *rbdProvisioner) parsePVSecret(namespace, secretName string) (string, error) { + if p.client == nil { + return "", fmt.Errorf("Cannot get kube client") + } + secrets, err := p.client.Core().Secrets(namespace).Get(secretName, metav1.GetOptions{}) + if err != nil { + return "", err + } + // TODO: Should we check secret.Type, like `k8s.io/kubernetes/pkg/volume/util.GetSecretForPV` function? + secret := "" + for k, v := range secrets.Data { + if k == secretKeyName { + return string(v), nil + } + secret = string(v) + } + + // If not found, the last secret in the map wins as done before + return secret, nil +} diff --git a/ceph/rbd/pkg/provision/rbd_util.go b/ceph/rbd/pkg/provision/rbd_util.go new file mode 100644 index 0000000000000000000000000000000000000000..8efb6d00c63025e8f1e7b71d5962ee005fdbab51 --- /dev/null +++ b/ceph/rbd/pkg/provision/rbd_util.go @@ -0,0 +1,154 @@ +/* +Copyright 2017 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 provision + +import ( + "fmt" + "math/rand" + "os/exec" + "strings" + + "github.com/golang/glog" + "github.com/kubernetes-incubator/external-storage/lib/controller" + "github.com/kubernetes-incubator/external-storage/lib/util" + "k8s.io/client-go/pkg/api/v1" +) + +const ( + imageWatcherStr = "watcher=" +) + +// RBDUtil is the utility structure to interact with the RBD. +type RBDUtil struct{} + +// CreateImage creates a new ceph image with provision and volume options. +func (u *RBDUtil) CreateImage(image string, pOpts *rbdProvisionOptions, options controller.VolumeOptions) (*v1.RBDVolumeSource, int, error) { + var output []byte + var err error + + capacity := options.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)] + volSizeBytes := capacity.Value() + // convert to MB that rbd defaults on + sz := int(util.RoundUpSize(volSizeBytes, 1024*1024)) + volSz := fmt.Sprintf("%d", sz) + // rbd create + l := len(pOpts.monitors) + // pick a mon randomly + start := rand.Int() % l + // iterate all monitors until create succeeds. + for i := start; i < start+l; i++ { + mon := pOpts.monitors[i%l] + if pOpts.imageFormat == rbdImageFormat2 { + glog.V(4).Infof("rbd: create %s size %s format %s (features: %s) using mon %s, pool %s id %s key %s", image, volSz, pOpts.imageFormat, pOpts.imageFeatures, mon, pOpts.pool, pOpts.adminID, pOpts.adminSecret) + } else { + glog.V(4).Infof("rbd: create %s size %s format %s using mon %s, pool %s id %s key %s", image, volSz, pOpts.imageFormat, mon, pOpts.pool, pOpts.adminID, pOpts.adminSecret) + } + args := []string{"create", image, "--size", volSz, "--pool", pOpts.pool, "--id", pOpts.adminID, "-m", mon, "--key=" + pOpts.adminSecret, "--image-format", pOpts.imageFormat} + if pOpts.imageFormat == rbdImageFormat2 { + // if no image features is provided, it results in empty string + // which disable all RBD image format 2 features as we expected + features := strings.Join(pOpts.imageFeatures, ",") + args = append(args, "--image-feature", features) + } + output, err = u.execCommand("rbd", args) + if err == nil { + break + } else { + glog.Warningf("failed to create rbd image, output %v", string(output)) + } + } + + if err != nil { + return nil, 0, fmt.Errorf("failed to create rbd image: %v, command output: %s", err, string(output)) + } + + return &v1.RBDVolumeSource{ + CephMonitors: pOpts.monitors, + RBDImage: image, + RBDPool: pOpts.pool, + }, sz, nil +} + +// rbdStatus checks if there is watcher on the image. +// It returns true if there is a watcher onthe image, otherwise returns false. +func (u *RBDUtil) rbdStatus(image string, pOpts *rbdProvisionOptions) (bool, error) { + var err error + var output string + var cmd []byte + + l := len(pOpts.monitors) + start := rand.Int() % l + // iterate all hosts until mount succeeds. + for i := start; i < start+l; i++ { + mon := pOpts.monitors[i%l] + // cmd "rbd status" list the rbd client watch with the following output: + // Watchers: + // watcher=10.16.153.105:0/710245699 client.14163 cookie=1 + glog.V(4).Infof("rbd: status %s using mon %s, pool %s id %s key %s", image, mon, pOpts.pool, pOpts.adminID, pOpts.adminSecret) + args := []string{"status", image, "--pool", pOpts.pool, "-m", mon, "--id", pOpts.adminID, "--key=" + pOpts.adminSecret} + cmd, err = u.execCommand("rbd", args) + output = string(cmd) + + if err != nil { + // ignore error code, just checkout output for watcher string + // TODO: Why should we ignore error code here? Igorning error code here cause we only try first monitor. + glog.Warningf("failed to execute rbd status on mon %s", mon) + } + + if strings.Contains(output, imageWatcherStr) { + glog.V(4).Infof("rbd: watchers on %s: %s", image, output) + return true, nil + } + glog.Warningf("rbd: no watchers on %s", image) + return false, nil + } + return false, nil +} + +// DeleteImage deletes a ceph image with provision and volume options. +func (u *RBDUtil) DeleteImage(image string, pOpts *rbdProvisionOptions) error { + var output []byte + found, err := u.rbdStatus(image, pOpts) + if err != nil { + return err + } + if found { + glog.Info("rbd is still being used ", image) + return fmt.Errorf("rbd %s is still being used", image) + } + // rbd rm + l := len(pOpts.monitors) + // pick a mon randomly + start := rand.Int() % l + // iterate all monitors until rm succeeds. + for i := start; i < start+l; i++ { + mon := pOpts.monitors[i%l] + glog.V(4).Infof("rbd: rm %s using mon %s, pool %s id %s key %s", image, mon, pOpts.pool, pOpts.adminID, pOpts.adminSecret) + args := []string{"rm", image, "--pool", pOpts.pool, "--id", pOpts.adminID, "-m", mon, "--key=" + pOpts.adminSecret} + output, err = u.execCommand("rbd", args) + if err == nil { + return nil + } + glog.Errorf("failed to delete rbd image: %v, command output: %s", err, string(output)) + } + return err +} + +func (u *RBDUtil) execCommand(command string, args []string) ([]byte, error) { + cmd := exec.Command(command, args...) + return cmd.CombinedOutput() +} diff --git a/ceph/rbd/secrets.yaml b/ceph/rbd/secrets.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f3b6d237ec3d85fb68045ff9185d12900623312f --- /dev/null +++ b/ceph/rbd/secrets.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Secret +metadata: + name: ceph-admin-secret + namespace: kube-system +type: "kubernetes.io/rbd" +data: + # ceph auth get-key client.admin | base64 + key: QVFCaUpWdFo5NW40TnhBQWVBZGU1M3NOeVd5UTExRTJ4bEZkOFE9PQ== +--- +apiVersion: v1 +kind: Secret +metadata: + name: ceph-secret +type: "kubernetes.io/rbd" +data: + # ceph auth add client.kube mon 'allow r' osd 'allow rwx pool=kube' + # ceph auth get-key client.kube | base64 + key: QVFDNkpWdFpLNCtSTEJBQUFLM2hCSTA0eU13ODZUd3hjRzlzK0E9PQ== diff --git a/ceph/rbd/test-pod.yaml b/ceph/rbd/test-pod.yaml new file mode 100644 index 0000000000000000000000000000000000000000..95ebc35842869b4e359ac695ce8ef29ecac83eb8 --- /dev/null +++ b/ceph/rbd/test-pod.yaml @@ -0,0 +1,21 @@ +kind: Pod +apiVersion: v1 +metadata: + name: test-pod +spec: + containers: + - name: test-pod + image: gcr.io/google_containers/busybox:1.24 + command: + - "/bin/sh" + args: + - "-c" + - "touch /mnt/SUCCESS && exit 0 || exit 1" + volumeMounts: + - name: pvc + mountPath: "/mnt" + restartPolicy: "Never" + volumes: + - name: pvc + persistentVolumeClaim: + claimName: claim1 diff --git a/test.sh b/test.sh index 503589fec6e745694ac689aaa54258c7cf29fbef..b142bd52ed06c1cfa3974e6861b1758f0c83b624 100755 --- a/test.sh +++ b/test.sh @@ -84,6 +84,7 @@ elif [ "$TEST_SUITE" = "everything-else" ]; then make aws/efs make test-aws/efs make ceph/cephfs + make ceph/rbd make flex make gluster/block make nfs-client