From ed9a1bbd3a9b689f4953deb6308d631314edf3f4 Mon Sep 17 00:00:00 2001 From: Huamin Chen Date: Tue, 11 Aug 2015 11:19:29 -0400 Subject: [PATCH] support fibre channel volume Signed-off-by: Huamin Chen --- api/swagger-spec/v1.json | 39 +++ cmd/kubelet/app/plugins.go | 2 + examples/examples_test.go | 3 + examples/fibre_channel/README.md | 77 ++++++ examples/fibre_channel/fc.yaml | 18 ++ pkg/api/deep_copy_generated.go | 37 +++ pkg/api/types.go | 20 ++ pkg/api/v1/conversion_generated.go | 80 ++++++ pkg/api/v1/deep_copy_generated.go | 37 +++ pkg/api/v1/types.go | 20 ++ pkg/api/v1/types_swagger_doc_generated.go | 14 + pkg/api/validation/validation.go | 27 ++ pkg/api/validation/validation_test.go | 8 +- pkg/apis/experimental/deep_copy_generated.go | 29 ++ .../experimental/v1/conversion_generated.go | 64 +++++ .../experimental/v1/deep_copy_generated.go | 29 ++ pkg/volume/fc/disk_manager.go | 112 ++++++++ pkg/volume/fc/doc.go | 19 ++ pkg/volume/fc/fc.go | 198 ++++++++++++++ pkg/volume/fc/fc_test.go | 252 ++++++++++++++++++ pkg/volume/fc/fc_util.go | 200 ++++++++++++++ pkg/volume/fc/fc_util_test.go | 91 +++++++ 22 files changed, 1375 insertions(+), 1 deletion(-) create mode 100644 examples/fibre_channel/README.md create mode 100644 examples/fibre_channel/fc.yaml create mode 100644 pkg/volume/fc/disk_manager.go create mode 100644 pkg/volume/fc/doc.go create mode 100644 pkg/volume/fc/fc.go create mode 100644 pkg/volume/fc/fc_test.go create mode 100644 pkg/volume/fc/fc_util.go create mode 100644 pkg/volume/fc/fc_util_test.go diff --git a/api/swagger-spec/v1.json b/api/swagger-spec/v1.json index 146bfb2eaf..4936139a99 100644 --- a/api/swagger-spec/v1.json +++ b/api/swagger-spec/v1.json @@ -12123,6 +12123,10 @@ "$ref": "v1.CephFSVolumeSource", "description": "CephFS represents a Ceph FS mount on the host that shares a pod's lifetime" }, + "fc": { + "$ref": "v1.FCVolumeSource", + "description": "FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod." + }, "accessModes": { "type": "array", "items": { @@ -12396,6 +12400,37 @@ } } }, + "v1.FCVolumeSource": { + "id": "v1.FCVolumeSource", + "description": "A Fibre Channel Disk can only be mounted as read/write once.", + "required": [ + "targetWWNs", + "lun", + "fsType" + ], + "properties": { + "targetWWNs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required: FC target world wide names (WWNs)" + }, + "lun": { + "type": "integer", + "format": "int32", + "description": "Required: FC target lun number" + }, + "fsType": { + "type": "string", + "description": "Required: Filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\"" + }, + "readOnly": { + "type": "boolean", + "description": "Optional: Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts." + } + } + }, "v1.PersistentVolumeStatus": { "id": "v1.PersistentVolumeStatus", "description": "PersistentVolumeStatus is the current status of a persistent volume.", @@ -12606,6 +12641,10 @@ "downwardAPI": { "$ref": "v1.DownwardAPIVolumeSource", "description": "DownwardAPI represents downward API about the pod that should populate this volume" + }, + "fc": { + "$ref": "v1.FCVolumeSource", + "description": "FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod." } } }, diff --git a/cmd/kubelet/app/plugins.go b/cmd/kubelet/app/plugins.go index 9afdb28533..9a5b5e0417 100644 --- a/cmd/kubelet/app/plugins.go +++ b/cmd/kubelet/app/plugins.go @@ -30,6 +30,7 @@ import ( "k8s.io/kubernetes/pkg/volume/cinder" "k8s.io/kubernetes/pkg/volume/downwardapi" "k8s.io/kubernetes/pkg/volume/empty_dir" + "k8s.io/kubernetes/pkg/volume/fc" "k8s.io/kubernetes/pkg/volume/gce_pd" "k8s.io/kubernetes/pkg/volume/git_repo" "k8s.io/kubernetes/pkg/volume/glusterfs" @@ -67,6 +68,7 @@ func ProbeVolumePlugins() []volume.VolumePlugin { allPlugins = append(allPlugins, cinder.ProbeVolumePlugins()...) allPlugins = append(allPlugins, cephfs.ProbeVolumePlugins()...) allPlugins = append(allPlugins, downwardapi.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, fc.ProbeVolumePlugins()...) return allPlugins } diff --git a/examples/examples_test.go b/examples/examples_test.go index 84f74af3ba..0e0f9e5bde 100644 --- a/examples/examples_test.go +++ b/examples/examples_test.go @@ -340,6 +340,9 @@ func TestExampleObjectSchemas(t *testing.T) { "cephfs": &api.Pod{}, "cephfs-with-secret": &api.Pod{}, }, + "../examples/fibre_channel": { + "fc": &api.Pod{}, + }, } capabilities.SetForTests(capabilities.Capabilities{ diff --git a/examples/fibre_channel/README.md b/examples/fibre_channel/README.md new file mode 100644 index 0000000000..d16b7d94ba --- /dev/null +++ b/examples/fibre_channel/README.md @@ -0,0 +1,77 @@ + + + + +WARNING +WARNING +WARNING +WARNING +WARNING + +

PLEASE NOTE: This document applies to the HEAD of the source tree

+ +If you are using a released version of Kubernetes, you should +refer to the docs that go with that version. + + +The latest 1.0.x release of this document can be found +[here](http://releases.k8s.io/release-1.0/examples/fibre_channel/README.md). + +Documentation for other releases can be found at +[releases.k8s.io](http://releases.k8s.io). + +-- + + + + + +## Step 1. Setting up Fibre Channel Target + +On your FC SAN Zone manager, allocate and mask LUNs so Kubernetes hosts can access them. + +## Step 2. Creating the Pod with Fibre Channel persistent storage + +Once you have installed Fibre Channel initiator and new Kubernetes, you can create a pod based on my example [fc.yaml](fc.yaml). In the pod JSON, you need to provide *targetWWNs* (array of Fibre Channel target's World Wide Names), *lun*, and the type of the filesystem that has been created on the lun, and *readOnly* boolean. + +Once your pod is created, run it on the Kubernetes master: + +```console +kubectl create -f ./your_new_pod.json +``` + +Here is my command and output: + +```console +# kubectl create -f examples/fibre_channel/fc.yaml +# kubectl get pods +NAME READY STATUS RESTARTS AGE +fcpd 2/2 Running 0 10m +``` + +On the Kubernetes host, I got these in mount output + +```console +#mount |grep /var/lib/kubelet/plugins/kubernetes.io +/dev/mapper/360a98000324669436c2b45666c567946 on /var/lib/kubelet/plugins/kubernetes.io/fc/500a0982991b8dc5-lun-2 type ext4 (ro,relatime,seclabel,stripe=16,data=ordered) +/dev/mapper/360a98000324669436c2b45666c567944 on /var/lib/kubelet/plugins/kubernetes.io/fc/500a0982991b8dc5-lun-1 type ext4 (rw,relatime,seclabel,stripe=16,data=ordered) +``` + +If you ssh to that machine, you can run `docker ps` to see the actual pod. + +```console +# docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +090ac457ddc2 kubernetes/pause "/pause" 12 minutes ago Up 12 minutes k8s_fcpd-rw.aae720ec_fcpd_default_4024318f-4121-11e5-a294-e839352ddd54_99eb5415 +5e2629cf3e7b kubernetes/pause "/pause" 12 minutes ago Up 12 minutes k8s_fcpd-ro.857720dc_fcpd_default_4024318f-4121-11e5-a294-e839352ddd54_c0175742 +2948683253f7 gcr.io/google_containers/pause:0.8.0 "/pause" 12 minutes ago Up 12 minutes k8s_POD.7be6d81d_fcpd_default_4024318f-4121-11e5-a294-e839352ddd54_8d9dd7bf +``` + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/examples/fibre_channel/README.md?pixel)]() + diff --git a/examples/fibre_channel/fc.yaml b/examples/fibre_channel/fc.yaml new file mode 100644 index 0000000000..ac28bee4a3 --- /dev/null +++ b/examples/fibre_channel/fc.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: fc +spec: + containers: + - image: kubernetes/pause + name: fc + volumeMounts: + - name: fc-vol + mountPath: /mnt/fc + volumes: + - name: fc-vol + fc: + targetWWNs: ['500a0982991b8dc5', '500a0982891b8dc5'] + lun: 2 + fsType: ext4 + readOnly: true diff --git a/pkg/api/deep_copy_generated.go b/pkg/api/deep_copy_generated.go index 0b050d8996..19c749c44c 100644 --- a/pkg/api/deep_copy_generated.go +++ b/pkg/api/deep_copy_generated.go @@ -551,6 +551,26 @@ func deepCopy_api_ExecAction(in ExecAction, out *ExecAction, c *conversion.Clone return nil } +func deepCopy_api_FCVolumeSource(in FCVolumeSource, out *FCVolumeSource, c *conversion.Cloner) error { + if in.TargetWWNs != nil { + out.TargetWWNs = make([]string, len(in.TargetWWNs)) + for i := range in.TargetWWNs { + out.TargetWWNs[i] = in.TargetWWNs[i] + } + } else { + out.TargetWWNs = nil + } + if in.Lun != nil { + out.Lun = new(int) + *out.Lun = *in.Lun + } else { + out.Lun = nil + } + out.FSType = in.FSType + out.ReadOnly = in.ReadOnly + return nil +} + func deepCopy_api_GCEPersistentDiskVolumeSource(in GCEPersistentDiskVolumeSource, out *GCEPersistentDiskVolumeSource, c *conversion.Cloner) error { out.PDName = in.PDName out.FSType = in.FSType @@ -1267,6 +1287,14 @@ func deepCopy_api_PersistentVolumeSource(in PersistentVolumeSource, out *Persist } else { out.CephFS = nil } + if in.FC != nil { + out.FC = new(FCVolumeSource) + if err := deepCopy_api_FCVolumeSource(*in.FC, out.FC, c); err != nil { + return err + } + } else { + out.FC = nil + } return nil } @@ -2155,6 +2183,14 @@ func deepCopy_api_VolumeSource(in VolumeSource, out *VolumeSource, c *conversion } else { out.DownwardAPI = nil } + if in.FC != nil { + out.FC = new(FCVolumeSource) + if err := deepCopy_api_FCVolumeSource(*in.FC, out.FC, c); err != nil { + return err + } + } else { + out.FC = nil + } return nil } @@ -2235,6 +2271,7 @@ func init() { deepCopy_api_EventList, deepCopy_api_EventSource, deepCopy_api_ExecAction, + deepCopy_api_FCVolumeSource, deepCopy_api_GCEPersistentDiskVolumeSource, deepCopy_api_GitRepoVolumeSource, deepCopy_api_GlusterfsVolumeSource, diff --git a/pkg/api/types.go b/pkg/api/types.go index f5593d308f..d99589ad1f 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -206,6 +206,8 @@ type VolumeSource struct { // DownwardAPI represents metadata about the pod that should populate this volume DownwardAPI *DownwardAPIVolumeSource `json:"downwardAPI,omitempty"` + // FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod. + FC *FCVolumeSource `json:"fc,omitempty"` } // Similar to VolumeSource but meant for the administrator who creates PVs. @@ -235,6 +237,8 @@ type PersistentVolumeSource struct { Cinder *CinderVolumeSource `json:"cinder,omitempty"` // CephFS represents a Ceph FS mount on the host that shares a pod's lifetime CephFS *CephFSVolumeSource `json:"cephfs,omitempty"` + // FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod. + FC *FCVolumeSource `json:"fc,omitempty"` } type PersistentVolumeClaimVolumeSource struct { @@ -452,6 +456,22 @@ type ISCSIVolumeSource struct { ReadOnly bool `json:"readOnly,omitempty"` } +// A Fibre Channel Disk can only be mounted as read/write once. +type FCVolumeSource struct { + // Required: FC target world wide names (WWNs) + TargetWWNs []string `json:"targetWWNs"` + // Required: FC target lun number + Lun *int `json:"lun"` + // Required: Filesystem type to mount. + // Must be a filesystem type supported by the host operating system. + // Ex. "ext4", "xfs", "ntfs" + // TODO: how do we prevent errors in the filesystem from compromising the machine + FSType string `json:"fsType"` + // Optional: Defaults to false (read/write). ReadOnly here will force + // the ReadOnly setting in VolumeMounts. + ReadOnly bool `json:"readOnly,omitempty"` +} + // AWSElasticBlockStoreVolumeSource represents a Persistent Disk resource in AWS. // // An AWS EBS disk must exist and be formatted before mounting to a container. diff --git a/pkg/api/v1/conversion_generated.go b/pkg/api/v1/conversion_generated.go index ce87e44140..b255cd7d5c 100644 --- a/pkg/api/v1/conversion_generated.go +++ b/pkg/api/v1/conversion_generated.go @@ -639,6 +639,29 @@ func convert_api_ExecAction_To_v1_ExecAction(in *api.ExecAction, out *ExecAction return nil } +func convert_api_FCVolumeSource_To_v1_FCVolumeSource(in *api.FCVolumeSource, out *FCVolumeSource, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*api.FCVolumeSource))(in) + } + if in.TargetWWNs != nil { + out.TargetWWNs = make([]string, len(in.TargetWWNs)) + for i := range in.TargetWWNs { + out.TargetWWNs[i] = in.TargetWWNs[i] + } + } else { + out.TargetWWNs = nil + } + if in.Lun != nil { + out.Lun = new(int) + *out.Lun = *in.Lun + } else { + out.Lun = nil + } + out.FSType = in.FSType + out.ReadOnly = in.ReadOnly + return nil +} + func convert_api_GCEPersistentDiskVolumeSource_To_v1_GCEPersistentDiskVolumeSource(in *api.GCEPersistentDiskVolumeSource, out *GCEPersistentDiskVolumeSource, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*api.GCEPersistentDiskVolumeSource))(in) @@ -1458,6 +1481,14 @@ func convert_api_PersistentVolumeSource_To_v1_PersistentVolumeSource(in *api.Per } else { out.CephFS = nil } + if in.FC != nil { + out.FC = new(FCVolumeSource) + if err := convert_api_FCVolumeSource_To_v1_FCVolumeSource(in.FC, out.FC, s); err != nil { + return err + } + } else { + out.FC = nil + } return nil } @@ -2385,6 +2416,14 @@ func convert_api_VolumeSource_To_v1_VolumeSource(in *api.VolumeSource, out *Volu } else { out.DownwardAPI = nil } + if in.FC != nil { + out.FC = new(FCVolumeSource) + if err := convert_api_FCVolumeSource_To_v1_FCVolumeSource(in.FC, out.FC, s); err != nil { + return err + } + } else { + out.FC = nil + } return nil } @@ -3001,6 +3040,29 @@ func convert_v1_ExecAction_To_api_ExecAction(in *ExecAction, out *api.ExecAction return nil } +func convert_v1_FCVolumeSource_To_api_FCVolumeSource(in *FCVolumeSource, out *api.FCVolumeSource, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*FCVolumeSource))(in) + } + if in.TargetWWNs != nil { + out.TargetWWNs = make([]string, len(in.TargetWWNs)) + for i := range in.TargetWWNs { + out.TargetWWNs[i] = in.TargetWWNs[i] + } + } else { + out.TargetWWNs = nil + } + if in.Lun != nil { + out.Lun = new(int) + *out.Lun = *in.Lun + } else { + out.Lun = nil + } + out.FSType = in.FSType + out.ReadOnly = in.ReadOnly + return nil +} + func convert_v1_GCEPersistentDiskVolumeSource_To_api_GCEPersistentDiskVolumeSource(in *GCEPersistentDiskVolumeSource, out *api.GCEPersistentDiskVolumeSource, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*GCEPersistentDiskVolumeSource))(in) @@ -3820,6 +3882,14 @@ func convert_v1_PersistentVolumeSource_To_api_PersistentVolumeSource(in *Persist } else { out.CephFS = nil } + if in.FC != nil { + out.FC = new(api.FCVolumeSource) + if err := convert_v1_FCVolumeSource_To_api_FCVolumeSource(in.FC, out.FC, s); err != nil { + return err + } + } else { + out.FC = nil + } return nil } @@ -4747,6 +4817,14 @@ func convert_v1_VolumeSource_To_api_VolumeSource(in *VolumeSource, out *api.Volu } else { out.DownwardAPI = nil } + if in.FC != nil { + out.FC = new(api.FCVolumeSource) + if err := convert_v1_FCVolumeSource_To_api_FCVolumeSource(in.FC, out.FC, s); err != nil { + return err + } + } else { + out.FC = nil + } return nil } @@ -4783,6 +4861,7 @@ func init() { convert_api_EventSource_To_v1_EventSource, convert_api_Event_To_v1_Event, convert_api_ExecAction_To_v1_ExecAction, + convert_api_FCVolumeSource_To_v1_FCVolumeSource, convert_api_GCEPersistentDiskVolumeSource_To_v1_GCEPersistentDiskVolumeSource, convert_api_GitRepoVolumeSource_To_v1_GitRepoVolumeSource, convert_api_GlusterfsVolumeSource_To_v1_GlusterfsVolumeSource, @@ -4897,6 +4976,7 @@ func init() { convert_v1_EventSource_To_api_EventSource, convert_v1_Event_To_api_Event, convert_v1_ExecAction_To_api_ExecAction, + convert_v1_FCVolumeSource_To_api_FCVolumeSource, convert_v1_GCEPersistentDiskVolumeSource_To_api_GCEPersistentDiskVolumeSource, convert_v1_GitRepoVolumeSource_To_api_GitRepoVolumeSource, convert_v1_GlusterfsVolumeSource_To_api_GlusterfsVolumeSource, diff --git a/pkg/api/v1/deep_copy_generated.go b/pkg/api/v1/deep_copy_generated.go index 9592cfa00e..6b37f01a8b 100644 --- a/pkg/api/v1/deep_copy_generated.go +++ b/pkg/api/v1/deep_copy_generated.go @@ -587,6 +587,26 @@ func deepCopy_v1_ExecAction(in ExecAction, out *ExecAction, c *conversion.Cloner return nil } +func deepCopy_v1_FCVolumeSource(in FCVolumeSource, out *FCVolumeSource, c *conversion.Cloner) error { + if in.TargetWWNs != nil { + out.TargetWWNs = make([]string, len(in.TargetWWNs)) + for i := range in.TargetWWNs { + out.TargetWWNs[i] = in.TargetWWNs[i] + } + } else { + out.TargetWWNs = nil + } + if in.Lun != nil { + out.Lun = new(int) + *out.Lun = *in.Lun + } else { + out.Lun = nil + } + out.FSType = in.FSType + out.ReadOnly = in.ReadOnly + return nil +} + func deepCopy_v1_GCEPersistentDiskVolumeSource(in GCEPersistentDiskVolumeSource, out *GCEPersistentDiskVolumeSource, c *conversion.Cloner) error { out.PDName = in.PDName out.FSType = in.FSType @@ -1287,6 +1307,14 @@ func deepCopy_v1_PersistentVolumeSource(in PersistentVolumeSource, out *Persiste } else { out.CephFS = nil } + if in.FC != nil { + out.FC = new(FCVolumeSource) + if err := deepCopy_v1_FCVolumeSource(*in.FC, out.FC, c); err != nil { + return err + } + } else { + out.FC = nil + } return nil } @@ -2181,6 +2209,14 @@ func deepCopy_v1_VolumeSource(in VolumeSource, out *VolumeSource, c *conversion. } else { out.DownwardAPI = nil } + if in.FC != nil { + out.FC = new(FCVolumeSource) + if err := deepCopy_v1_FCVolumeSource(*in.FC, out.FC, c); err != nil { + return err + } + } else { + out.FC = nil + } return nil } @@ -2240,6 +2276,7 @@ func init() { deepCopy_v1_EventList, deepCopy_v1_EventSource, deepCopy_v1_ExecAction, + deepCopy_v1_FCVolumeSource, deepCopy_v1_GCEPersistentDiskVolumeSource, deepCopy_v1_GitRepoVolumeSource, deepCopy_v1_GlusterfsVolumeSource, diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 62cebcd3d5..05a94e43c7 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -252,6 +252,8 @@ type VolumeSource struct { // DownwardAPI represents downward API about the pod that should populate this volume DownwardAPI *DownwardAPIVolumeSource `json:"downwardAPI,omitempty"` + // FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod. + FC *FCVolumeSource `json:"fc,omitempty"` } // PersistentVolumeClaimVolumeSource references the user's PVC in the same namespace. @@ -302,6 +304,8 @@ type PersistentVolumeSource struct { Cinder *CinderVolumeSource `json:"cinder,omitempty"` // CephFS represents a Ceph FS mount on the host that shares a pod's lifetime CephFS *CephFSVolumeSource `json:"cephfs,omitempty"` + // FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod. + FC *FCVolumeSource `json:"fc,omitempty"` } // PersistentVolume (PV) is a storage resource provisioned by an administrator. @@ -706,6 +710,22 @@ type ISCSIVolumeSource struct { ReadOnly bool `json:"readOnly,omitempty"` } +// A Fibre Channel Disk can only be mounted as read/write once. +type FCVolumeSource struct { + // Required: FC target world wide names (WWNs) + TargetWWNs []string `json:"targetWWNs"` + // Required: FC target lun number + Lun *int `json:"lun"` + // Required: Filesystem type to mount. + // Must be a filesystem type supported by the host operating system. + // Ex. "ext4", "xfs", "ntfs" + // TODO: how do we prevent errors in the filesystem from compromising the machine + FSType string `json:"fsType"` + // Optional: Defaults to false (read/write). ReadOnly here will force + // the ReadOnly setting in VolumeMounts. + ReadOnly bool `json:"readOnly,omitempty"` +} + // ContainerPort represents a network port in a single container. type ContainerPort struct { // If specified, this must be an IANA_SVC_NAME and unique within the pod. Each diff --git a/pkg/api/v1/types_swagger_doc_generated.go b/pkg/api/v1/types_swagger_doc_generated.go index 08db845bcc..f298d911f8 100644 --- a/pkg/api/v1/types_swagger_doc_generated.go +++ b/pkg/api/v1/types_swagger_doc_generated.go @@ -387,6 +387,18 @@ func (ExecAction) SwaggerDoc() map[string]string { return map_ExecAction } +var map_FCVolumeSource = map[string]string{ + "": "A Fibre Channel Disk can only be mounted as read/write once.", + "targetWWNs": "Required: FC target world wide names (WWNs)", + "lun": "Required: FC target lun number", + "fsType": "Required: Filesystem type to mount. Must be a filesystem type supported by the host operating system. Ex. \"ext4\", \"xfs\", \"ntfs\"", + "readOnly": "Optional: Defaults to false (read/write). ReadOnly here will force the ReadOnly setting in VolumeMounts.", +} + +func (FCVolumeSource) SwaggerDoc() map[string]string { + return map_FCVolumeSource +} + var map_GCEPersistentDiskVolumeSource = map[string]string{ "": "GCEPersistentDiskVolumeSource represents a Persistent Disk resource in Google Compute Engine.\n\nA GCE PD must exist and be formatted before mounting to a container. The disk must also be in the same GCE project and zone as the kubelet. A GCE PD can only be mounted as read/write once.", "pdName": "Unique name of the PD resource in GCE. Used to identify the disk in GCE. More info: http://releases.k8s.io/HEAD/docs/user-guide/volumes.md#gcepersistentdisk", @@ -844,6 +856,7 @@ var map_PersistentVolumeSource = map[string]string{ "iscsi": "ISCSI represents an ISCSI Disk resource that is attached to a kubelet's host machine and then exposed to the pod. Provisioned by an admin.", "cinder": "Cinder represents a cinder volume attached and mounted on kubelets host machine More info: http://releases.k8s.io/HEAD/examples/mysql-cinder-pd/README.md", "cephfs": "CephFS represents a Ceph FS mount on the host that shares a pod's lifetime", + "fc": "FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod.", } func (PersistentVolumeSource) SwaggerDoc() map[string]string { @@ -1376,6 +1389,7 @@ var map_VolumeSource = map[string]string{ "cinder": "Cinder represents a cinder volume attached and mounted on kubelets host machine More info: http://releases.k8s.io/HEAD/examples/mysql-cinder-pd/README.md", "cephfs": "CephFS represents a Ceph FS mount on the host that shares a pod's lifetime", "downwardAPI": "DownwardAPI represents downward API about the pod that should populate this volume", + "fc": "FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod.", } func (VolumeSource) SwaggerDoc() map[string]string { diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index d4142b71c0..9cd3628057 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -390,6 +390,10 @@ func validateSource(source *api.VolumeSource) errs.ValidationErrorList { numVolumes++ allErrs = append(allErrs, validateDownwardAPIVolumeSource(source.DownwardAPI).Prefix("downwardApi")...) } + if source.FC != nil { + numVolumes++ + allErrs = append(allErrs, validateFCVolumeSource(source.FC).Prefix("fc")...) + } if numVolumes != 1 { allErrs = append(allErrs, errs.NewFieldInvalid("", source, "exactly 1 volume type is required")) } @@ -430,6 +434,25 @@ func validateISCSIVolumeSource(iscsi *api.ISCSIVolumeSource) errs.ValidationErro return allErrs } +func validateFCVolumeSource(fc *api.FCVolumeSource) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if len(fc.TargetWWNs) < 1 { + allErrs = append(allErrs, errs.NewFieldRequired("targetWWNs")) + } + if fc.FSType == "" { + allErrs = append(allErrs, errs.NewFieldRequired("fsType")) + } + + if fc.Lun == nil { + allErrs = append(allErrs, errs.NewFieldRequired("lun")) + } else { + if *fc.Lun < 0 || *fc.Lun > 255 { + allErrs = append(allErrs, errs.NewFieldInvalid("lun", fc.Lun, "")) + } + } + return allErrs +} + func validateGCEPersistentDiskVolumeSource(PD *api.GCEPersistentDiskVolumeSource) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} if PD.PDName == "" { @@ -624,6 +647,10 @@ func ValidatePersistentVolume(pv *api.PersistentVolume) errs.ValidationErrorList numVolumes++ allErrs = append(allErrs, validateCinderVolumeSource(pv.Spec.Cinder).Prefix("cinder")...) } + if pv.Spec.FC != nil { + numVolumes++ + allErrs = append(allErrs, validateFCVolumeSource(pv.Spec.FC).Prefix("fc")...) + } if numVolumes != 1 { allErrs = append(allErrs, errs.NewFieldInvalid("", pv.Spec.PersistentVolumeSource, "exactly 1 volume type is required")) } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index b376db62b2..614d21079c 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -446,6 +446,7 @@ func TestValidatePersistentVolumeClaim(t *testing.T) { } func TestValidateVolumes(t *testing.T) { + lun := 1 successCase := []api.Volume{ {Name: "abc", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{Path: "/mnt/path1"}}}, {Name: "123", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{Path: "/mnt/path2"}}}, @@ -486,12 +487,13 @@ func TestValidateVolumes(t *testing.T) { APIVersion: "v1", FieldPath: "metadata.labels"}}, }}}}, + {Name: "fc", VolumeSource: api.VolumeSource{FC: &api.FCVolumeSource{[]string{"some_wwn"}, &lun, "ext4", false}}}, } names, errs := validateVolumes(successCase) if len(errs) != 0 { t.Errorf("expected success: %v", errs) } - if len(names) != len(successCase) || !names.HasAll("abc", "123", "abc-123", "empty", "gcepd", "gitrepo", "secret", "iscsidisk", "cinder", "cephfs") { + if len(names) != len(successCase) || !names.HasAll("abc", "123", "abc-123", "empty", "gcepd", "gitrepo", "secret", "iscsidisk", "cinder", "cephfs", "fc") { t.Errorf("wrong names result: %v", names) } emptyVS := api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}} @@ -527,6 +529,8 @@ func TestValidateVolumes(t *testing.T) { APIVersion: "v1", FieldPath: "metadata.labels"}}}, }} + zeroWWN := api.VolumeSource{FC: &api.FCVolumeSource{[]string{}, &lun, "ext4", false}} + emptyLun := api.VolumeSource{FC: &api.FCVolumeSource{[]string{"wwn"}, nil, "ext4", false}} errorCases := map[string]struct { V []api.Volume T errors.ValidationErrorType @@ -549,6 +553,8 @@ func TestValidateVolumes(t *testing.T) { "dot dot path": {[]api.Volume{{Name: "dotdotpath", VolumeSource: dotDotInPath}}, errors.ValidationErrorTypeInvalid, "[0].source.downwardApi.path", "must not contain \"..\"."}, "dot dot file name": {[]api.Volume{{Name: "dotdotfilename", VolumeSource: dotDotPathName}}, errors.ValidationErrorTypeInvalid, "[0].source.downwardApi.path", "must not start with \"..\"."}, "dot dot first level dirent ": {[]api.Volume{{Name: "dotdotdirfilename", VolumeSource: dotDotFirstLevelDirent}}, errors.ValidationErrorTypeInvalid, "[0].source.downwardApi.path", "must not start with \"..\"."}, + "empty wwn": {[]api.Volume{{Name: "badimage", VolumeSource: zeroWWN}}, errors.ValidationErrorTypeRequired, "[0].source.fc.targetWWNs", ""}, + "empty lun": {[]api.Volume{{Name: "badimage", VolumeSource: emptyLun}}, errors.ValidationErrorTypeRequired, "[0].source.fc.lun", ""}, } for k, v := range errorCases { _, errs := validateVolumes(v.V) diff --git a/pkg/apis/experimental/deep_copy_generated.go b/pkg/apis/experimental/deep_copy_generated.go index bb080f6622..1ce12d71cd 100644 --- a/pkg/apis/experimental/deep_copy_generated.go +++ b/pkg/apis/experimental/deep_copy_generated.go @@ -253,6 +253,26 @@ func deepCopy_api_ExecAction(in api.ExecAction, out *api.ExecAction, c *conversi return nil } +func deepCopy_api_FCVolumeSource(in api.FCVolumeSource, out *api.FCVolumeSource, c *conversion.Cloner) error { + if in.TargetWWNs != nil { + out.TargetWWNs = make([]string, len(in.TargetWWNs)) + for i := range in.TargetWWNs { + out.TargetWWNs[i] = in.TargetWWNs[i] + } + } else { + out.TargetWWNs = nil + } + if in.Lun != nil { + out.Lun = new(int) + *out.Lun = *in.Lun + } else { + out.Lun = nil + } + out.FSType = in.FSType + out.ReadOnly = in.ReadOnly + return nil +} + func deepCopy_api_GCEPersistentDiskVolumeSource(in api.GCEPersistentDiskVolumeSource, out *api.GCEPersistentDiskVolumeSource, c *conversion.Cloner) error { out.PDName = in.PDName out.FSType = in.FSType @@ -728,6 +748,14 @@ func deepCopy_api_VolumeSource(in api.VolumeSource, out *api.VolumeSource, c *co } else { out.DownwardAPI = nil } + if in.FC != nil { + out.FC = new(api.FCVolumeSource) + if err := deepCopy_api_FCVolumeSource(*in.FC, out.FC, c); err != nil { + return err + } + } else { + out.FC = nil + } return nil } @@ -1281,6 +1309,7 @@ func init() { deepCopy_api_EnvVar, deepCopy_api_EnvVarSource, deepCopy_api_ExecAction, + deepCopy_api_FCVolumeSource, deepCopy_api_GCEPersistentDiskVolumeSource, deepCopy_api_GitRepoVolumeSource, deepCopy_api_GlusterfsVolumeSource, diff --git a/pkg/apis/experimental/v1/conversion_generated.go b/pkg/apis/experimental/v1/conversion_generated.go index 6feae3022c..0f20d6535b 100644 --- a/pkg/apis/experimental/v1/conversion_generated.go +++ b/pkg/apis/experimental/v1/conversion_generated.go @@ -288,6 +288,29 @@ func convert_api_ExecAction_To_v1_ExecAction(in *api.ExecAction, out *v1.ExecAct return nil } +func convert_api_FCVolumeSource_To_v1_FCVolumeSource(in *api.FCVolumeSource, out *v1.FCVolumeSource, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*api.FCVolumeSource))(in) + } + if in.TargetWWNs != nil { + out.TargetWWNs = make([]string, len(in.TargetWWNs)) + for i := range in.TargetWWNs { + out.TargetWWNs[i] = in.TargetWWNs[i] + } + } else { + out.TargetWWNs = nil + } + if in.Lun != nil { + out.Lun = new(int) + *out.Lun = *in.Lun + } else { + out.Lun = nil + } + out.FSType = in.FSType + out.ReadOnly = in.ReadOnly + return nil +} + func convert_api_GCEPersistentDiskVolumeSource_To_v1_GCEPersistentDiskVolumeSource(in *api.GCEPersistentDiskVolumeSource, out *v1.GCEPersistentDiskVolumeSource, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*api.GCEPersistentDiskVolumeSource))(in) @@ -774,6 +797,14 @@ func convert_api_VolumeSource_To_v1_VolumeSource(in *api.VolumeSource, out *v1.V } else { out.DownwardAPI = nil } + if in.FC != nil { + out.FC = new(v1.FCVolumeSource) + if err := convert_api_FCVolumeSource_To_v1_FCVolumeSource(in.FC, out.FC, s); err != nil { + return err + } + } else { + out.FC = nil + } return nil } @@ -1037,6 +1068,29 @@ func convert_v1_ExecAction_To_api_ExecAction(in *v1.ExecAction, out *api.ExecAct return nil } +func convert_v1_FCVolumeSource_To_api_FCVolumeSource(in *v1.FCVolumeSource, out *api.FCVolumeSource, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*v1.FCVolumeSource))(in) + } + if in.TargetWWNs != nil { + out.TargetWWNs = make([]string, len(in.TargetWWNs)) + for i := range in.TargetWWNs { + out.TargetWWNs[i] = in.TargetWWNs[i] + } + } else { + out.TargetWWNs = nil + } + if in.Lun != nil { + out.Lun = new(int) + *out.Lun = *in.Lun + } else { + out.Lun = nil + } + out.FSType = in.FSType + out.ReadOnly = in.ReadOnly + return nil +} + func convert_v1_GCEPersistentDiskVolumeSource_To_api_GCEPersistentDiskVolumeSource(in *v1.GCEPersistentDiskVolumeSource, out *api.GCEPersistentDiskVolumeSource, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*v1.GCEPersistentDiskVolumeSource))(in) @@ -1523,6 +1577,14 @@ func convert_v1_VolumeSource_To_api_VolumeSource(in *v1.VolumeSource, out *api.V } else { out.DownwardAPI = nil } + if in.FC != nil { + out.FC = new(api.FCVolumeSource) + if err := convert_v1_FCVolumeSource_To_api_FCVolumeSource(in.FC, out.FC, s); err != nil { + return err + } + } else { + out.FC = nil + } return nil } @@ -2572,6 +2634,7 @@ func init() { convert_api_EnvVarSource_To_v1_EnvVarSource, convert_api_EnvVar_To_v1_EnvVar, convert_api_ExecAction_To_v1_ExecAction, + convert_api_FCVolumeSource_To_v1_FCVolumeSource, convert_api_GCEPersistentDiskVolumeSource_To_v1_GCEPersistentDiskVolumeSource, convert_api_GitRepoVolumeSource_To_v1_GitRepoVolumeSource, convert_api_GlusterfsVolumeSource_To_v1_GlusterfsVolumeSource, @@ -2643,6 +2706,7 @@ func init() { convert_v1_EnvVarSource_To_api_EnvVarSource, convert_v1_EnvVar_To_api_EnvVar, convert_v1_ExecAction_To_api_ExecAction, + convert_v1_FCVolumeSource_To_api_FCVolumeSource, convert_v1_GCEPersistentDiskVolumeSource_To_api_GCEPersistentDiskVolumeSource, convert_v1_GitRepoVolumeSource_To_api_GitRepoVolumeSource, convert_v1_GlusterfsVolumeSource_To_api_GlusterfsVolumeSource, diff --git a/pkg/apis/experimental/v1/deep_copy_generated.go b/pkg/apis/experimental/v1/deep_copy_generated.go index 084ce72476..1d3621ca76 100644 --- a/pkg/apis/experimental/v1/deep_copy_generated.go +++ b/pkg/apis/experimental/v1/deep_copy_generated.go @@ -291,6 +291,26 @@ func deepCopy_v1_ExecAction(in v1.ExecAction, out *v1.ExecAction, c *conversion. return nil } +func deepCopy_v1_FCVolumeSource(in v1.FCVolumeSource, out *v1.FCVolumeSource, c *conversion.Cloner) error { + if in.TargetWWNs != nil { + out.TargetWWNs = make([]string, len(in.TargetWWNs)) + for i := range in.TargetWWNs { + out.TargetWWNs[i] = in.TargetWWNs[i] + } + } else { + out.TargetWWNs = nil + } + if in.Lun != nil { + out.Lun = new(int) + *out.Lun = *in.Lun + } else { + out.Lun = nil + } + out.FSType = in.FSType + out.ReadOnly = in.ReadOnly + return nil +} + func deepCopy_v1_GCEPersistentDiskVolumeSource(in v1.GCEPersistentDiskVolumeSource, out *v1.GCEPersistentDiskVolumeSource, c *conversion.Cloner) error { out.PDName = in.PDName out.FSType = in.FSType @@ -767,6 +787,14 @@ func deepCopy_v1_VolumeSource(in v1.VolumeSource, out *v1.VolumeSource, c *conve } else { out.DownwardAPI = nil } + if in.FC != nil { + out.FC = new(v1.FCVolumeSource) + if err := deepCopy_v1_FCVolumeSource(*in.FC, out.FC, c); err != nil { + return err + } + } else { + out.FC = nil + } return nil } @@ -1307,6 +1335,7 @@ func init() { deepCopy_v1_EnvVar, deepCopy_v1_EnvVarSource, deepCopy_v1_ExecAction, + deepCopy_v1_FCVolumeSource, deepCopy_v1_GCEPersistentDiskVolumeSource, deepCopy_v1_GitRepoVolumeSource, deepCopy_v1_GlusterfsVolumeSource, diff --git a/pkg/volume/fc/disk_manager.go b/pkg/volume/fc/disk_manager.go new file mode 100644 index 0000000000..b7419ecc95 --- /dev/null +++ b/pkg/volume/fc/disk_manager.go @@ -0,0 +1,112 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 fc + +import ( + "os" + + "github.com/golang/glog" + "k8s.io/kubernetes/pkg/util/mount" +) + +// Abstract interface to disk operations. +type diskManager interface { + MakeGlobalPDName(disk fcDisk) string + // Attaches the disk to the kubelet's host machine. + AttachDisk(b fcDiskBuilder) error + // Detaches the disk from the kubelet's host machine. + DetachDisk(disk fcDiskCleaner, mntPath string) error +} + +// utility to mount a disk based filesystem +func diskSetUp(manager diskManager, b fcDiskBuilder, volPath string, mounter mount.Interface) error { + globalPDPath := manager.MakeGlobalPDName(*b.fcDisk) + // TODO: handle failed mounts here. + noMnt, err := mounter.IsLikelyNotMountPoint(volPath) + + if err != nil && !os.IsNotExist(err) { + glog.Errorf("cannot validate mountpoint: %s", volPath) + return err + } + if !noMnt { + return nil + } + if err := manager.AttachDisk(b); err != nil { + glog.Errorf("failed to attach disk") + return err + } + + if err := os.MkdirAll(volPath, 0750); err != nil { + glog.Errorf("failed to mkdir:%s", volPath) + return err + } + // Perform a bind mount to the full path to allow duplicate mounts of the same disk. + options := []string{"bind"} + if b.readOnly { + options = append(options, "ro") + } + err = mounter.Mount(globalPDPath, volPath, "", options) + if err != nil { + glog.Errorf("failed to bind mount:%s", globalPDPath) + return err + } + return nil +} + +// utility to tear down a disk based filesystem +func diskTearDown(manager diskManager, c fcDiskCleaner, volPath string, mounter mount.Interface) error { + noMnt, err := mounter.IsLikelyNotMountPoint(volPath) + if err != nil { + glog.Errorf("cannot validate mountpoint %s", volPath) + return err + } + if noMnt { + return os.Remove(volPath) + } + + refs, err := mount.GetMountRefs(mounter, volPath) + if err != nil { + glog.Errorf("failed to get reference count %s", volPath) + return err + } + if err := mounter.Unmount(volPath); err != nil { + glog.Errorf("failed to unmount %s", volPath) + return err + } + // If len(refs) is 1, then all bind mounts have been removed, and the + // remaining reference is the global mount. It is safe to detach. + if len(refs) == 1 { + mntPath := refs[0] + if err := manager.DetachDisk(c, mntPath); err != nil { + glog.Errorf("failed to detach disk from %s", mntPath) + return err + } + } + + noMnt, mntErr := mounter.IsLikelyNotMountPoint(volPath) + if mntErr != nil { + glog.Errorf("isMountpoint check failed: %v", mntErr) + return err + } + if noMnt { + if err := os.Remove(volPath); err != nil { + return err + } + } + return nil + +} diff --git a/pkg/volume/fc/doc.go b/pkg/volume/fc/doc.go new file mode 100644 index 0000000000..454597efd5 --- /dev/null +++ b/pkg/volume/fc/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 fc contains the internal representation of +// Fibre Channel (fc) volumes. +package fc diff --git a/pkg/volume/fc/fc.go b/pkg/volume/fc/fc.go new file mode 100644 index 0000000000..534fd25e59 --- /dev/null +++ b/pkg/volume/fc/fc.go @@ -0,0 +1,198 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 fc + +import ( + "fmt" + "strconv" + + "github.com/golang/glog" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/types" + "k8s.io/kubernetes/pkg/util" + "k8s.io/kubernetes/pkg/util/exec" + "k8s.io/kubernetes/pkg/util/mount" + "k8s.io/kubernetes/pkg/volume" +) + +// This is the primary entrypoint for volume plugins. +func ProbeVolumePlugins() []volume.VolumePlugin { + return []volume.VolumePlugin{&fcPlugin{nil, exec.New()}} +} + +type fcPlugin struct { + host volume.VolumeHost + exe exec.Interface +} + +var _ volume.VolumePlugin = &fcPlugin{} +var _ volume.PersistentVolumePlugin = &fcPlugin{} + +const ( + fcPluginName = "kubernetes.io/fc" +) + +func (plugin *fcPlugin) Init(host volume.VolumeHost) { + plugin.host = host +} + +func (plugin *fcPlugin) Name() string { + return fcPluginName +} + +func (plugin *fcPlugin) CanSupport(spec *volume.Spec) bool { + if (spec.Volume != nil && spec.Volume.FC == nil) || (spec.PersistentVolume != nil && spec.PersistentVolume.Spec.FC == nil) { + return false + } + // TODO: turn this into a func so CanSupport can be unit tested without + // having to make system calls + // see if /sys/class/fc_transport is there, which indicates fc is connected + _, err := plugin.execCommand("ls", []string{"/sys/class/fc_transport"}) + if err == nil { + return true + } + + return false +} + +func (plugin *fcPlugin) GetAccessModes() []api.PersistentVolumeAccessMode { + return []api.PersistentVolumeAccessMode{ + api.ReadWriteOnce, + api.ReadOnlyMany, + } +} + +func (plugin *fcPlugin) NewBuilder(spec *volume.Spec, pod *api.Pod, _ volume.VolumeOptions) (volume.Builder, error) { + // Inject real implementations here, test through the internal function. + return plugin.newBuilderInternal(spec, pod.UID, &FCUtil{}, plugin.host.GetMounter()) +} + +func (plugin *fcPlugin) newBuilderInternal(spec *volume.Spec, podUID types.UID, manager diskManager, mounter mount.Interface) (volume.Builder, error) { + // fc volumes used directly in a pod have a ReadOnly flag set by the pod author. + // fc volumes used as a PersistentVolume gets the ReadOnly flag indirectly through the persistent-claim volume used to mount the PV + var readOnly bool + var fc *api.FCVolumeSource + if spec.Volume != nil && spec.Volume.FC != nil { + fc = spec.Volume.FC + readOnly = fc.ReadOnly + } else { + fc = spec.PersistentVolume.Spec.FC + readOnly = spec.ReadOnly + } + + if fc.Lun == nil { + return nil, fmt.Errorf("empty lun") + } + + lun := strconv.Itoa(*fc.Lun) + + return &fcDiskBuilder{ + fcDisk: &fcDisk{ + podUID: podUID, + volName: spec.Name(), + wwns: fc.TargetWWNs, + lun: lun, + manager: manager, + mounter: &mount.SafeFormatAndMount{mounter, exec.New()}, + io: &osIOHandler{}, + plugin: plugin}, + fsType: fc.FSType, + readOnly: readOnly, + }, nil +} + +func (plugin *fcPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) { + // Inject real implementations here, test through the internal function. + return plugin.newCleanerInternal(volName, podUID, &FCUtil{}, plugin.host.GetMounter()) +} + +func (plugin *fcPlugin) newCleanerInternal(volName string, podUID types.UID, manager diskManager, mounter mount.Interface) (volume.Cleaner, error) { + return &fcDiskCleaner{&fcDisk{ + podUID: podUID, + volName: volName, + manager: manager, + mounter: mounter, + plugin: plugin, + io: &osIOHandler{}, + }}, nil +} + +func (plugin *fcPlugin) execCommand(command string, args []string) ([]byte, error) { + cmd := plugin.exe.Command(command, args...) + return cmd.CombinedOutput() +} + +type fcDisk struct { + volName string + podUID types.UID + portal string + wwns []string + lun string + plugin *fcPlugin + mounter mount.Interface + // Utility interface that provides API calls to the provider to attach/detach disks. + manager diskManager + // io handler interface + io ioHandler +} + +func (fc *fcDisk) GetPath() string { + name := fcPluginName + // safe to use PodVolumeDir now: volume teardown occurs before pod is cleaned up + return fc.plugin.host.GetPodVolumeDir(fc.podUID, util.EscapeQualifiedNameForDisk(name), fc.volName) +} + +type fcDiskBuilder struct { + *fcDisk + readOnly bool + fsType string +} + +var _ volume.Builder = &fcDiskBuilder{} + +func (b *fcDiskBuilder) SetUp() error { + return b.SetUpAt(b.GetPath()) +} + +func (b *fcDiskBuilder) SetUpAt(dir string) error { + // diskSetUp checks mountpoints and prevent repeated calls + err := diskSetUp(b.manager, *b, dir, b.mounter) + if err != nil { + glog.Errorf("fc: failed to setup") + } + return err +} + +type fcDiskCleaner struct { + *fcDisk +} + +var _ volume.Cleaner = &fcDiskCleaner{} + +func (b *fcDiskBuilder) IsReadOnly() bool { + return b.readOnly +} + +// Unmounts the bind mount, and detaches the disk only if the disk +// resource was the last reference to that disk on the kubelet. +func (c *fcDiskCleaner) TearDown() error { + return c.TearDownAt(c.GetPath()) +} + +func (c *fcDiskCleaner) TearDownAt(dir string) error { + return diskTearDown(c.manager, *c, dir, c.mounter) +} diff --git a/pkg/volume/fc/fc_test.go b/pkg/volume/fc/fc_test.go new file mode 100644 index 0000000000..e92cc41dfb --- /dev/null +++ b/pkg/volume/fc/fc_test.go @@ -0,0 +1,252 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 fc + +import ( + "os" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/testapi" + "k8s.io/kubernetes/pkg/client/unversioned/testclient" + "k8s.io/kubernetes/pkg/types" + "k8s.io/kubernetes/pkg/util/mount" + "k8s.io/kubernetes/pkg/volume" +) + +func TestCanSupport(t *testing.T) { + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost("/tmp/fake", nil, nil)) + + plug, err := plugMgr.FindPluginByName("kubernetes.io/fc") + if err != nil { + t.Errorf("Can't find the plugin by name") + } + if plug.Name() != "kubernetes.io/fc" { + t.Errorf("Wrong name: %s", plug.Name()) + } + if plug.CanSupport(&volume.Spec{Volume: &api.Volume{VolumeSource: api.VolumeSource{}}}) { + t.Errorf("Expected false") + } +} + +func TestGetAccessModes(t *testing.T) { + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost("/tmp/fake", nil, nil)) + + plug, err := plugMgr.FindPersistentPluginByName("kubernetes.io/fc") + if err != nil { + t.Errorf("Can't find the plugin by name") + } + if !contains(plug.GetAccessModes(), api.ReadWriteOnce) || !contains(plug.GetAccessModes(), api.ReadOnlyMany) { + t.Errorf("Expected two AccessModeTypes: %s and %s", api.ReadWriteOnce, api.ReadOnlyMany) + } +} + +func contains(modes []api.PersistentVolumeAccessMode, mode api.PersistentVolumeAccessMode) bool { + for _, m := range modes { + if m == mode { + return true + } + } + return false +} + +type fakeDiskManager struct { + attachCalled bool + detachCalled bool +} + +func (fake *fakeDiskManager) MakeGlobalPDName(disk fcDisk) string { + return "/tmp/fake_fc_path" +} +func (fake *fakeDiskManager) AttachDisk(b fcDiskBuilder) error { + globalPath := b.manager.MakeGlobalPDName(*b.fcDisk) + err := os.MkdirAll(globalPath, 0750) + if err != nil { + return err + } + // Simulate the global mount so that the fakeMounter returns the + // expected number of mounts for the attached disk. + b.mounter.Mount(globalPath, globalPath, b.fsType, nil) + + fake.attachCalled = true + return nil +} + +func (fake *fakeDiskManager) DetachDisk(c fcDiskCleaner, mntPath string) error { + globalPath := c.manager.MakeGlobalPDName(*c.fcDisk) + err := os.RemoveAll(globalPath) + if err != nil { + return err + } + fake.detachCalled = true + return nil +} + +func doTestPlugin(t *testing.T, spec *volume.Spec) { + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost("/tmp/fake", nil, nil)) + + plug, err := plugMgr.FindPluginByName("kubernetes.io/fc") + if err != nil { + t.Errorf("Can't find the plugin by name") + } + fakeManager := &fakeDiskManager{} + fakeMounter := &mount.FakeMounter{} + builder, err := plug.(*fcPlugin).newBuilderInternal(spec, types.UID("poduid"), fakeManager, fakeMounter) + if err != nil { + t.Errorf("Failed to make a new Builder: %v", err) + } + if builder == nil { + t.Errorf("Got a nil Builder: %v") + } + + path := builder.GetPath() + if path != "/tmp/fake/pods/poduid/volumes/kubernetes.io~fc/vol1" { + t.Errorf("Got unexpected path: %s", path) + } + + if err := builder.SetUp(); err != nil { + t.Errorf("Expected success, got: %v", err) + } + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + t.Errorf("SetUp() failed, volume path not created: %s", path) + } else { + t.Errorf("SetUp() failed: %v", err) + } + } + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + t.Errorf("SetUp() failed, volume path not created: %s", path) + } else { + t.Errorf("SetUp() failed: %v", err) + } + } + if !fakeManager.attachCalled { + t.Errorf("Attach was not called") + } + + fakeManager = &fakeDiskManager{} + cleaner, err := plug.(*fcPlugin).newCleanerInternal("vol1", types.UID("poduid"), fakeManager, fakeMounter) + if err != nil { + t.Errorf("Failed to make a new Cleaner: %v", err) + } + if cleaner == nil { + t.Errorf("Got a nil Cleaner: %v") + } + + if err := cleaner.TearDown(); err != nil { + t.Errorf("Expected success, got: %v", err) + } + if _, err := os.Stat(path); err == nil { + t.Errorf("TearDown() failed, volume path still exists: %s", path) + } else if !os.IsNotExist(err) { + t.Errorf("SetUp() failed: %v", err) + } + if !fakeManager.detachCalled { + t.Errorf("Detach was not called") + } +} + +func TestPluginVolume(t *testing.T) { + lun := 0 + vol := &api.Volume{ + Name: "vol1", + VolumeSource: api.VolumeSource{ + FC: &api.FCVolumeSource{ + TargetWWNs: []string{"some_wwn"}, + FSType: "ext4", + Lun: &lun, + }, + }, + } + doTestPlugin(t, volume.NewSpecFromVolume(vol)) +} + +func TestPluginPersistentVolume(t *testing.T) { + lun := 0 + vol := &api.PersistentVolume{ + ObjectMeta: api.ObjectMeta{ + Name: "vol1", + }, + Spec: api.PersistentVolumeSpec{ + PersistentVolumeSource: api.PersistentVolumeSource{ + FC: &api.FCVolumeSource{ + TargetWWNs: []string{"some_wwn"}, + FSType: "ext4", + Lun: &lun, + }, + }, + }, + } + doTestPlugin(t, volume.NewSpecFromPersistentVolume(vol, false)) +} + +func TestPersistentClaimReadOnlyFlag(t *testing.T) { + lun := 0 + pv := &api.PersistentVolume{ + ObjectMeta: api.ObjectMeta{ + Name: "pvA", + }, + Spec: api.PersistentVolumeSpec{ + PersistentVolumeSource: api.PersistentVolumeSource{ + FC: &api.FCVolumeSource{ + TargetWWNs: []string{"some_wwn"}, + FSType: "ext4", + Lun: &lun, + }, + }, + ClaimRef: &api.ObjectReference{ + Name: "claimA", + }, + }, + } + + claim := &api.PersistentVolumeClaim{ + ObjectMeta: api.ObjectMeta{ + Name: "claimA", + Namespace: "nsA", + }, + Spec: api.PersistentVolumeClaimSpec{ + VolumeName: "pvA", + }, + Status: api.PersistentVolumeClaimStatus{ + Phase: api.ClaimBound, + }, + } + + o := testclient.NewObjects(api.Scheme, api.Scheme) + o.Add(pv) + o.Add(claim) + client := &testclient.Fake{} + client.AddReactor("*", "*", testclient.ObjectReaction(o, testapi.Default.RESTMapper())) + + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeVolumeHost("/tmp/fake", client, nil)) + plug, _ := plugMgr.FindPluginByName(fcPluginName) + + // readOnly bool is supplied by persistent-claim volume source when its builder creates other volumes + spec := volume.NewSpecFromPersistentVolume(pv, true) + pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: types.UID("poduid")}} + builder, _ := plug.NewBuilder(spec, pod, volume.VolumeOptions{}) + + if !builder.IsReadOnly() { + t.Errorf("Expected true for builder.IsReadOnly") + } +} diff --git a/pkg/volume/fc/fc_util.go b/pkg/volume/fc/fc_util.go new file mode 100644 index 0000000000..b0dbc82b36 --- /dev/null +++ b/pkg/volume/fc/fc_util.go @@ -0,0 +1,200 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 fc + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + + "github.com/golang/glog" + "k8s.io/kubernetes/pkg/volume" +) + +type ioHandler interface { + ReadDir(dirname string) ([]os.FileInfo, error) + Lstat(name string) (os.FileInfo, error) + EvalSymlinks(path string) (string, error) + WriteFile(filename string, data []byte, perm os.FileMode) error +} + +type osIOHandler struct{} + +func (handler *osIOHandler) ReadDir(dirname string) ([]os.FileInfo, error) { + return ioutil.ReadDir(dirname) +} +func (handler *osIOHandler) Lstat(name string) (os.FileInfo, error) { + return os.Lstat(name) +} +func (handler *osIOHandler) EvalSymlinks(path string) (string, error) { + return filepath.EvalSymlinks(path) +} +func (handler *osIOHandler) WriteFile(filename string, data []byte, perm os.FileMode) error { + return ioutil.WriteFile(filename, data, perm) +} + +// given a disk path like /dev/sdx, find the devicemapper parent +func findMultipathDeviceMapper(disk string, io ioHandler) string { + sys_path := "/sys/block/" + if dirs, err := io.ReadDir(sys_path); err == nil { + for _, f := range dirs { + name := f.Name() + if strings.HasPrefix(name, "dm-") { + if _, err1 := io.Lstat(sys_path + name + "/slaves/" + disk); err1 == nil { + return "/dev/" + name + } + } + } + } + return "" +} + +// given a wwn and lun, find the device and associated devicemapper parent +func findDisk(wwn, lun string, io ioHandler) (string, string) { + fc_path := "-fc-0x" + wwn + "-lun-" + lun + dev_path := "/dev/disk/by-path/" + if dirs, err := io.ReadDir(dev_path); err == nil { + for _, f := range dirs { + name := f.Name() + if strings.Contains(name, fc_path) { + if disk, err1 := io.EvalSymlinks(dev_path + name); err1 == nil { + arr := strings.Split(disk, "/") + l := len(arr) - 1 + dev := arr[l] + dm := findMultipathDeviceMapper(dev, io) + return disk, dm + } + } + } + } + return "", "" +} + +func createMultipathConf(path string, io ioHandler) { + if _, err := os.Lstat(path); err != nil { + data := []byte(`defaults { + find_multipaths yes + user_friendly_names yes +} + + +blacklist { +} +`) + io.WriteFile(path, data, 0664) + } +} + +// rescan scsi bus +func scsiHostRescan(io ioHandler) { + scsi_path := "/sys/class/scsi_host/" + if dirs, err := io.ReadDir(scsi_path); err == nil { + for _, f := range dirs { + name := scsi_path + f.Name() + "/scan" + data := []byte("- - -") + io.WriteFile(name, data, 0666) + } + } +} + +// make a directory like /var/lib/kubelet/plugins/kubernetes.io/pod/fc/target-lun-0 +func makePDNameInternal(host volume.VolumeHost, wwns []string, lun string) string { + return path.Join(host.GetPluginDir(fcPluginName), wwns[0]+"-lun-"+lun) +} + +type FCUtil struct{} + +func (util *FCUtil) MakeGlobalPDName(fc fcDisk) string { + return makePDNameInternal(fc.plugin.host, fc.wwns, fc.lun) +} + +func searchDisk(wwns []string, lun string, io ioHandler) (string, string) { + disk := "" + dm := "" + + rescaned := false + // two-phase search: + // first phase, search existing device path, if a multipath dm is found, exit loop + // otherwise, in second phase, rescan scsi bus and search again, return with any findings + for true { + for _, wwn := range wwns { + disk, dm = findDisk(wwn, lun, io) + // if multipath device is found, break + if dm != "" { + break + } + } + // if a dm is found, exit loop + if rescaned || dm != "" { + break + } + // rescan and search again + // create multipath conf if it is not there + createMultipathConf("/etc/multipath.conf", io) + // rescan scsi bus + scsiHostRescan(io) + rescaned = true + } + return disk, dm +} + +func (util *FCUtil) AttachDisk(b fcDiskBuilder) error { + devicePath := "" + wwns := b.wwns + lun := b.lun + io := b.io + disk, dm := searchDisk(wwns, lun, io) + // if no disk matches input wwn and lun, exit + if disk == "" && dm == "" { + return fmt.Errorf("no fc disk found") + } + + // if multipath devicemapper device is found, use it; otherwise use raw disk + if dm != "" { + devicePath = dm + } else { + devicePath = disk + } + // mount it + globalPDPath := b.manager.MakeGlobalPDName(*b.fcDisk) + noMnt, err := b.mounter.IsLikelyNotMountPoint(globalPDPath) + if !noMnt { + glog.Infof("fc: %s already mounted", globalPDPath) + return nil + } + + if err := os.MkdirAll(globalPDPath, 0750); err != nil { + return fmt.Errorf("fc: failed to mkdir %s, error", globalPDPath) + } + + err = b.mounter.Mount(devicePath, globalPDPath, b.fsType, nil) + if err != nil { + return fmt.Errorf("fc: failed to mount fc volume %s [%s] to %s, error %v", devicePath, b.fsType, globalPDPath, err) + } + + return err +} + +func (util *FCUtil) DetachDisk(c fcDiskCleaner, mntPath string) error { + if err := c.mounter.Unmount(mntPath); err != nil { + return fmt.Errorf("fc detach disk: failed to unmount: %s\nError: %v", mntPath, err) + } + return nil +} diff --git a/pkg/volume/fc/fc_util_test.go b/pkg/volume/fc/fc_util_test.go new file mode 100644 index 0000000000..dd47cf6ee7 --- /dev/null +++ b/pkg/volume/fc/fc_util_test.go @@ -0,0 +1,91 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +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 fc + +import ( + "os" + "testing" + "time" +) + +type fakeFileInfo struct { + name string +} + +func (fi *fakeFileInfo) Name() string { + return fi.name +} + +func (fi *fakeFileInfo) Size() int64 { + return 0 +} + +func (fi *fakeFileInfo) Mode() os.FileMode { + return 777 +} + +func (fi *fakeFileInfo) ModTime() time.Time { + return time.Now() +} +func (fi *fakeFileInfo) IsDir() bool { + return false +} + +func (fi *fakeFileInfo) Sys() interface{} { + return nil +} + +type fakeIOHandler struct{} + +func (handler *fakeIOHandler) ReadDir(dirname string) ([]os.FileInfo, error) { + switch dirname { + case "/dev/disk/by-path/": + f := &fakeFileInfo{ + name: "pci-0000:41:00.0-fc-0x500a0981891b8dc5-lun-0", + } + return []os.FileInfo{f}, nil + case "/sys/block/": + f := &fakeFileInfo{ + name: "dm-1", + } + return []os.FileInfo{f}, nil + } + return nil, nil +} + +func (handler *fakeIOHandler) Lstat(name string) (os.FileInfo, error) { + return nil, nil +} + +func (handler *fakeIOHandler) EvalSymlinks(path string) (string, error) { + return "/dev/sda", nil +} + +func (handler *fakeIOHandler) WriteFile(filename string, data []byte, perm os.FileMode) error { + return nil +} + +func TestIoHandler(t *testing.T) { + io := &fakeIOHandler{} + wwns := []string{"500a0981891b8dc5"} + lun := "0" + disk, dm := searchDisk(wwns, lun, io) + // if no disk matches input wwn and lun, exit + if disk == "" && dm == "" { + t.Errorf("no fc disk found") + } +}