github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/plugins/csi/plugin.go (about) 1 package csi 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 8 csipbv1 "github.com/container-storage-interface/spec/lib/go/csi" 9 "github.com/hashicorp/nomad/nomad/structs" 10 "github.com/hashicorp/nomad/plugins/base" 11 "google.golang.org/grpc" 12 ) 13 14 // CSIPlugin implements a lightweight abstraction layer around a CSI Plugin. 15 // It validates that responses from storage providers (SP's), correctly conform 16 // to the specification before returning response data or erroring. 17 type CSIPlugin interface { 18 base.BasePlugin 19 20 // PluginProbe is used to verify that the plugin is in a healthy state 21 PluginProbe(ctx context.Context) (bool, error) 22 23 // PluginGetInfo is used to return semantic data about the plugin. 24 // Response: 25 // - string: name, the name of the plugin in domain notation format. 26 // - string: version, the vendor version of the plugin 27 PluginGetInfo(ctx context.Context) (string, string, error) 28 29 // PluginGetCapabilities is used to return the available capabilities from the 30 // identity service. This currently only looks for the CONTROLLER_SERVICE and 31 // Accessible Topology Support 32 PluginGetCapabilities(ctx context.Context) (*PluginCapabilitySet, error) 33 34 // GetControllerCapabilities is used to get controller-specific capabilities 35 // for a plugin. 36 ControllerGetCapabilities(ctx context.Context) (*ControllerCapabilitySet, error) 37 38 // ControllerPublishVolume is used to attach a remote volume to a cluster node. 39 ControllerPublishVolume(ctx context.Context, req *ControllerPublishVolumeRequest, opts ...grpc.CallOption) (*ControllerPublishVolumeResponse, error) 40 41 // ControllerUnpublishVolume is used to deattach a remote volume from a cluster node. 42 ControllerUnpublishVolume(ctx context.Context, req *ControllerUnpublishVolumeRequest, opts ...grpc.CallOption) (*ControllerUnpublishVolumeResponse, error) 43 44 // ControllerValidateCapabilities is used to validate that a volume exists and 45 // supports the requested capability. 46 ControllerValidateCapabilities(ctx context.Context, volumeID string, capabilities *VolumeCapability, secrets structs.CSISecrets, opts ...grpc.CallOption) error 47 48 // NodeGetCapabilities is used to return the available capabilities from the 49 // Node Service. 50 NodeGetCapabilities(ctx context.Context) (*NodeCapabilitySet, error) 51 52 // NodeGetInfo is used to return semantic data about the current node in 53 // respect to the SP. 54 NodeGetInfo(ctx context.Context) (*NodeGetInfoResponse, error) 55 56 // NodeStageVolume is used when a plugin has the STAGE_UNSTAGE volume capability 57 // to prepare a volume for usage on a host. If err == nil, the response should 58 // be assumed to be successful. 59 NodeStageVolume(ctx context.Context, volumeID string, publishContext map[string]string, stagingTargetPath string, capabilities *VolumeCapability, secrets structs.CSISecrets, opts ...grpc.CallOption) error 60 61 // NodeUnstageVolume is used when a plugin has the STAGE_UNSTAGE volume capability 62 // to undo the work performed by NodeStageVolume. If a volume has been staged, 63 // this RPC must be called before freeing the volume. 64 // 65 // If err == nil, the response should be assumed to be successful. 66 NodeUnstageVolume(ctx context.Context, volumeID string, stagingTargetPath string, opts ...grpc.CallOption) error 67 68 // NodePublishVolume is used to prepare a volume for use by an allocation. 69 // if err == nil the response should be assumed to be successful. 70 NodePublishVolume(ctx context.Context, req *NodePublishVolumeRequest, opts ...grpc.CallOption) error 71 72 // NodeUnpublishVolume is used to cleanup usage of a volume for an alloc. This 73 // MUST be called before calling NodeUnstageVolume or ControllerUnpublishVolume 74 // for the given volume. 75 NodeUnpublishVolume(ctx context.Context, volumeID, targetPath string, opts ...grpc.CallOption) error 76 77 // Shutdown the client and ensure any connections are cleaned up. 78 Close() error 79 } 80 81 type NodePublishVolumeRequest struct { 82 // The external ID of the volume to publish. 83 ExternalID string 84 85 // If the volume was attached via a call to `ControllerPublishVolume` then 86 // we need to provide the returned PublishContext here. 87 PublishContext map[string]string 88 89 // The path to which the volume was staged by `NodeStageVolume`. 90 // It MUST be an absolute path in the root filesystem of the process 91 // serving this request. 92 // E.g {the plugins internal mount path}/staging/volumeid/... 93 // 94 // It MUST be set if the Node Plugin implements the 95 // `STAGE_UNSTAGE_VOLUME` node capability. 96 StagingTargetPath string 97 98 // The path to which the volume will be published. 99 // It MUST be an absolute path in the root filesystem of the process serving this 100 // request. 101 // E.g {the plugins internal mount path}/per-alloc/allocid/volumeid/... 102 // 103 // The CO SHALL ensure uniqueness of target_path per volume. 104 // The CO SHALL ensure that the parent directory of this path exists 105 // and that the process serving the request has `read` and `write` 106 // permissions to that parent directory. 107 TargetPath string 108 109 // Volume capability describing how the CO intends to use this volume. 110 VolumeCapability *VolumeCapability 111 112 Readonly bool 113 114 // Secrets required by plugins to complete the node publish volume 115 // request. This field is OPTIONAL. 116 Secrets structs.CSISecrets 117 } 118 119 func (r *NodePublishVolumeRequest) ToCSIRepresentation() *csipbv1.NodePublishVolumeRequest { 120 if r == nil { 121 return nil 122 } 123 124 return &csipbv1.NodePublishVolumeRequest{ 125 VolumeId: r.ExternalID, 126 PublishContext: r.PublishContext, 127 StagingTargetPath: r.StagingTargetPath, 128 TargetPath: r.TargetPath, 129 VolumeCapability: r.VolumeCapability.ToCSIRepresentation(), 130 Readonly: r.Readonly, 131 Secrets: r.Secrets, 132 } 133 } 134 135 func (r *NodePublishVolumeRequest) Validate() error { 136 if r.ExternalID == "" { 137 return errors.New("missing volume ID") 138 } 139 140 if r.TargetPath == "" { 141 return errors.New("missing TargetPath") 142 } 143 144 if r.VolumeCapability == nil { 145 return errors.New("missing VolumeCapabilities") 146 } 147 148 return nil 149 } 150 151 type PluginCapabilitySet struct { 152 hasControllerService bool 153 hasTopologies bool 154 } 155 156 func (p *PluginCapabilitySet) HasControllerService() bool { 157 return p.hasControllerService 158 } 159 160 // HasTopologies indicates whether the volumes for this plugin are equally 161 // accessible by all nodes in the cluster. 162 // If true, we MUST use the topology information when scheduling workloads. 163 func (p *PluginCapabilitySet) HasToplogies() bool { 164 return p.hasTopologies 165 } 166 167 func (p *PluginCapabilitySet) IsEqual(o *PluginCapabilitySet) bool { 168 return p.hasControllerService == o.hasControllerService && p.hasTopologies == o.hasTopologies 169 } 170 171 func NewTestPluginCapabilitySet(topologies, controller bool) *PluginCapabilitySet { 172 return &PluginCapabilitySet{ 173 hasTopologies: topologies, 174 hasControllerService: controller, 175 } 176 } 177 178 func NewPluginCapabilitySet(capabilities *csipbv1.GetPluginCapabilitiesResponse) *PluginCapabilitySet { 179 cs := &PluginCapabilitySet{} 180 181 pluginCapabilities := capabilities.GetCapabilities() 182 183 for _, pcap := range pluginCapabilities { 184 if svcCap := pcap.GetService(); svcCap != nil { 185 switch svcCap.Type { 186 case csipbv1.PluginCapability_Service_UNKNOWN: 187 continue 188 case csipbv1.PluginCapability_Service_CONTROLLER_SERVICE: 189 cs.hasControllerService = true 190 case csipbv1.PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS: 191 cs.hasTopologies = true 192 default: 193 continue 194 } 195 } 196 } 197 198 return cs 199 } 200 201 type ControllerCapabilitySet struct { 202 HasPublishUnpublishVolume bool 203 HasPublishReadonly bool 204 HasListVolumes bool 205 HasListVolumesPublishedNodes bool 206 } 207 208 func NewControllerCapabilitySet(resp *csipbv1.ControllerGetCapabilitiesResponse) *ControllerCapabilitySet { 209 cs := &ControllerCapabilitySet{} 210 211 pluginCapabilities := resp.GetCapabilities() 212 for _, pcap := range pluginCapabilities { 213 if c := pcap.GetRpc(); c != nil { 214 switch c.Type { 215 case csipbv1.ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME: 216 cs.HasPublishUnpublishVolume = true 217 case csipbv1.ControllerServiceCapability_RPC_PUBLISH_READONLY: 218 cs.HasPublishReadonly = true 219 case csipbv1.ControllerServiceCapability_RPC_LIST_VOLUMES: 220 cs.HasListVolumes = true 221 case csipbv1.ControllerServiceCapability_RPC_LIST_VOLUMES_PUBLISHED_NODES: 222 cs.HasListVolumesPublishedNodes = true 223 default: 224 continue 225 } 226 } 227 } 228 229 return cs 230 } 231 232 type ControllerPublishVolumeRequest struct { 233 ExternalID string 234 NodeID string 235 ReadOnly bool 236 VolumeCapability *VolumeCapability 237 Secrets structs.CSISecrets 238 // VolumeContext map[string]string // TODO: https://github.com/hashicorp/nomad/issues/7771 239 } 240 241 func (r *ControllerPublishVolumeRequest) ToCSIRepresentation() *csipbv1.ControllerPublishVolumeRequest { 242 if r == nil { 243 return nil 244 } 245 246 return &csipbv1.ControllerPublishVolumeRequest{ 247 VolumeId: r.ExternalID, 248 NodeId: r.NodeID, 249 Readonly: r.ReadOnly, 250 VolumeCapability: r.VolumeCapability.ToCSIRepresentation(), 251 Secrets: r.Secrets, 252 // VolumeContext: r.VolumeContext, https://github.com/hashicorp/nomad/issues/7771 253 } 254 } 255 256 func (r *ControllerPublishVolumeRequest) Validate() error { 257 if r.ExternalID == "" { 258 return errors.New("missing volume ID") 259 } 260 if r.NodeID == "" { 261 return errors.New("missing NodeID") 262 } 263 return nil 264 } 265 266 type ControllerPublishVolumeResponse struct { 267 PublishContext map[string]string 268 } 269 270 type ControllerUnpublishVolumeRequest struct { 271 ExternalID string 272 NodeID string 273 Secrets structs.CSISecrets 274 } 275 276 func (r *ControllerUnpublishVolumeRequest) ToCSIRepresentation() *csipbv1.ControllerUnpublishVolumeRequest { 277 if r == nil { 278 return nil 279 } 280 281 return &csipbv1.ControllerUnpublishVolumeRequest{ 282 VolumeId: r.ExternalID, 283 NodeId: r.NodeID, 284 Secrets: r.Secrets, 285 } 286 } 287 288 func (r *ControllerUnpublishVolumeRequest) Validate() error { 289 if r.ExternalID == "" { 290 return errors.New("missing ExternalID") 291 } 292 if r.NodeID == "" { 293 // the spec allows this but it would unpublish the 294 // volume from all nodes 295 return errors.New("missing NodeID") 296 } 297 return nil 298 } 299 300 type ControllerUnpublishVolumeResponse struct{} 301 302 type NodeCapabilitySet struct { 303 HasStageUnstageVolume bool 304 } 305 306 func NewNodeCapabilitySet(resp *csipbv1.NodeGetCapabilitiesResponse) *NodeCapabilitySet { 307 cs := &NodeCapabilitySet{} 308 pluginCapabilities := resp.GetCapabilities() 309 for _, pcap := range pluginCapabilities { 310 if c := pcap.GetRpc(); c != nil { 311 switch c.Type { 312 case csipbv1.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME: 313 cs.HasStageUnstageVolume = true 314 default: 315 continue 316 } 317 } 318 } 319 320 return cs 321 } 322 323 // VolumeAccessMode represents the desired access mode of the CSI Volume 324 type VolumeAccessMode csipbv1.VolumeCapability_AccessMode_Mode 325 326 var _ fmt.Stringer = VolumeAccessModeUnknown 327 328 var ( 329 VolumeAccessModeUnknown = VolumeAccessMode(csipbv1.VolumeCapability_AccessMode_UNKNOWN) 330 VolumeAccessModeSingleNodeWriter = VolumeAccessMode(csipbv1.VolumeCapability_AccessMode_SINGLE_NODE_WRITER) 331 VolumeAccessModeSingleNodeReaderOnly = VolumeAccessMode(csipbv1.VolumeCapability_AccessMode_SINGLE_NODE_READER_ONLY) 332 VolumeAccessModeMultiNodeReaderOnly = VolumeAccessMode(csipbv1.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY) 333 VolumeAccessModeMultiNodeSingleWriter = VolumeAccessMode(csipbv1.VolumeCapability_AccessMode_MULTI_NODE_SINGLE_WRITER) 334 VolumeAccessModeMultiNodeMultiWriter = VolumeAccessMode(csipbv1.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER) 335 ) 336 337 func (a VolumeAccessMode) String() string { 338 return a.ToCSIRepresentation().String() 339 } 340 341 func (a VolumeAccessMode) ToCSIRepresentation() csipbv1.VolumeCapability_AccessMode_Mode { 342 return csipbv1.VolumeCapability_AccessMode_Mode(a) 343 } 344 345 // VolumeAccessType represents the filesystem apis that the user intends to use 346 // with the volume. E.g whether it will be used as a block device or if they wish 347 // to have a mounted filesystem. 348 type VolumeAccessType int32 349 350 var _ fmt.Stringer = VolumeAccessTypeBlock 351 352 var ( 353 VolumeAccessTypeBlock VolumeAccessType = 1 354 VolumeAccessTypeMount VolumeAccessType = 2 355 ) 356 357 func (v VolumeAccessType) String() string { 358 if v == VolumeAccessTypeBlock { 359 return "VolumeAccessType.Block" 360 } else if v == VolumeAccessTypeMount { 361 return "VolumeAccessType.Mount" 362 } else { 363 return "VolumeAccessType.Unspecified" 364 } 365 } 366 367 // VolumeCapability describes the overall usage requirements for a given CSI Volume 368 type VolumeCapability struct { 369 AccessType VolumeAccessType 370 AccessMode VolumeAccessMode 371 372 // Indicate that the volume will be accessed via the filesystem API. 373 MountVolume *structs.CSIMountOptions 374 } 375 376 func VolumeCapabilityFromStructs(sAccessType structs.CSIVolumeAttachmentMode, sAccessMode structs.CSIVolumeAccessMode) (*VolumeCapability, error) { 377 var accessType VolumeAccessType 378 switch sAccessType { 379 case structs.CSIVolumeAttachmentModeBlockDevice: 380 accessType = VolumeAccessTypeBlock 381 case structs.CSIVolumeAttachmentModeFilesystem: 382 accessType = VolumeAccessTypeMount 383 default: 384 // These fields are validated during job submission, but here we perform a 385 // final check during transformation into the requisite CSI Data type to 386 // defend against development bugs and corrupted state - and incompatible 387 // nomad versions in the future. 388 return nil, fmt.Errorf("Unknown volume attachment mode: %s", sAccessType) 389 } 390 391 var accessMode VolumeAccessMode 392 switch sAccessMode { 393 case structs.CSIVolumeAccessModeSingleNodeReader: 394 accessMode = VolumeAccessModeSingleNodeReaderOnly 395 case structs.CSIVolumeAccessModeSingleNodeWriter: 396 accessMode = VolumeAccessModeSingleNodeWriter 397 case structs.CSIVolumeAccessModeMultiNodeMultiWriter: 398 accessMode = VolumeAccessModeMultiNodeMultiWriter 399 case structs.CSIVolumeAccessModeMultiNodeSingleWriter: 400 accessMode = VolumeAccessModeMultiNodeSingleWriter 401 case structs.CSIVolumeAccessModeMultiNodeReader: 402 accessMode = VolumeAccessModeMultiNodeReaderOnly 403 default: 404 // These fields are validated during job submission, but here we perform a 405 // final check during transformation into the requisite CSI Data type to 406 // defend against development bugs and corrupted state - and incompatible 407 // nomad versions in the future. 408 return nil, fmt.Errorf("Unknown volume access mode: %v", sAccessMode) 409 } 410 411 return &VolumeCapability{ 412 AccessType: accessType, 413 AccessMode: accessMode, 414 }, nil 415 } 416 417 func (c *VolumeCapability) ToCSIRepresentation() *csipbv1.VolumeCapability { 418 if c == nil { 419 return nil 420 } 421 422 vc := &csipbv1.VolumeCapability{ 423 AccessMode: &csipbv1.VolumeCapability_AccessMode{ 424 Mode: c.AccessMode.ToCSIRepresentation(), 425 }, 426 } 427 428 if c.AccessType == VolumeAccessTypeMount { 429 opts := &csipbv1.VolumeCapability_MountVolume{} 430 if c.MountVolume != nil { 431 opts.FsType = c.MountVolume.FSType 432 opts.MountFlags = c.MountVolume.MountFlags 433 } 434 vc.AccessType = &csipbv1.VolumeCapability_Mount{Mount: opts} 435 } else { 436 vc.AccessType = &csipbv1.VolumeCapability_Block{Block: &csipbv1.VolumeCapability_BlockVolume{}} 437 } 438 439 return vc 440 }