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  }