github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/plugins/csi/client.go (about)

     1  package csi
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math"
     7  	"net"
     8  	"time"
     9  
    10  	csipbv1 "github.com/container-storage-interface/spec/lib/go/csi"
    11  	"github.com/hashicorp/go-hclog"
    12  	multierror "github.com/hashicorp/go-multierror"
    13  	"github.com/hashicorp/nomad/helper"
    14  	"github.com/hashicorp/nomad/helper/grpc-middleware/logging"
    15  	"github.com/hashicorp/nomad/nomad/structs"
    16  	"github.com/hashicorp/nomad/plugins/base"
    17  	"github.com/hashicorp/nomad/plugins/shared/hclspec"
    18  	"google.golang.org/grpc"
    19  )
    20  
    21  // PluginTypeCSI implements the CSI plugin interface
    22  const PluginTypeCSI = "csi"
    23  
    24  type NodeGetInfoResponse struct {
    25  	NodeID             string
    26  	MaxVolumes         int64
    27  	AccessibleTopology *Topology
    28  }
    29  
    30  // Topology is a map of topological domains to topological segments.
    31  // A topological domain is a sub-division of a cluster, like "region",
    32  // "zone", "rack", etc.
    33  //
    34  // According to CSI, there are a few requirements for the keys within this map:
    35  // - Valid keys have two segments: an OPTIONAL prefix and name, separated
    36  //   by a slash (/), for example: "com.company.example/zone".
    37  // - The key name segment is REQUIRED. The prefix is OPTIONAL.
    38  // - The key name MUST be 63 characters or less, begin and end with an
    39  //   alphanumeric character ([a-z0-9A-Z]), and contain only dashes (-),
    40  //   underscores (_), dots (.), or alphanumerics in between, for example
    41  //   "zone".
    42  // - The key prefix MUST be 63 characters or less, begin and end with a
    43  //   lower-case alphanumeric character ([a-z0-9]), contain only
    44  //   dashes (-), dots (.), or lower-case alphanumerics in between, and
    45  //   follow domain name notation format
    46  //   (https://tools.ietf.org/html/rfc1035#section-2.3.1).
    47  // - The key prefix SHOULD include the plugin's host company name and/or
    48  //   the plugin name, to minimize the possibility of collisions with keys
    49  //   from other plugins.
    50  // - If a key prefix is specified, it MUST be identical across all
    51  //   topology keys returned by the SP (across all RPCs).
    52  // - Keys MUST be case-insensitive. Meaning the keys "Zone" and "zone"
    53  //   MUST not both exist.
    54  // - Each value (topological segment) MUST contain 1 or more strings.
    55  // - Each string MUST be 63 characters or less and begin and end with an
    56  //   alphanumeric character with '-', '_', '.', or alphanumerics in
    57  //   between.
    58  type Topology struct {
    59  	Segments map[string]string
    60  }
    61  
    62  // CSIControllerClient defines the minimal CSI Controller Plugin interface used
    63  // by nomad to simplify the interface required for testing.
    64  type CSIControllerClient interface {
    65  	ControllerGetCapabilities(ctx context.Context, in *csipbv1.ControllerGetCapabilitiesRequest, opts ...grpc.CallOption) (*csipbv1.ControllerGetCapabilitiesResponse, error)
    66  	ControllerPublishVolume(ctx context.Context, in *csipbv1.ControllerPublishVolumeRequest, opts ...grpc.CallOption) (*csipbv1.ControllerPublishVolumeResponse, error)
    67  	ControllerUnpublishVolume(ctx context.Context, in *csipbv1.ControllerUnpublishVolumeRequest, opts ...grpc.CallOption) (*csipbv1.ControllerUnpublishVolumeResponse, error)
    68  	ValidateVolumeCapabilities(ctx context.Context, in *csipbv1.ValidateVolumeCapabilitiesRequest, opts ...grpc.CallOption) (*csipbv1.ValidateVolumeCapabilitiesResponse, error)
    69  }
    70  
    71  // CSINodeClient defines the minimal CSI Node Plugin interface used
    72  // by nomad to simplify the interface required for testing.
    73  type CSINodeClient interface {
    74  	NodeGetCapabilities(ctx context.Context, in *csipbv1.NodeGetCapabilitiesRequest, opts ...grpc.CallOption) (*csipbv1.NodeGetCapabilitiesResponse, error)
    75  	NodeGetInfo(ctx context.Context, in *csipbv1.NodeGetInfoRequest, opts ...grpc.CallOption) (*csipbv1.NodeGetInfoResponse, error)
    76  	NodeStageVolume(ctx context.Context, in *csipbv1.NodeStageVolumeRequest, opts ...grpc.CallOption) (*csipbv1.NodeStageVolumeResponse, error)
    77  	NodeUnstageVolume(ctx context.Context, in *csipbv1.NodeUnstageVolumeRequest, opts ...grpc.CallOption) (*csipbv1.NodeUnstageVolumeResponse, error)
    78  	NodePublishVolume(ctx context.Context, in *csipbv1.NodePublishVolumeRequest, opts ...grpc.CallOption) (*csipbv1.NodePublishVolumeResponse, error)
    79  	NodeUnpublishVolume(ctx context.Context, in *csipbv1.NodeUnpublishVolumeRequest, opts ...grpc.CallOption) (*csipbv1.NodeUnpublishVolumeResponse, error)
    80  }
    81  
    82  type client struct {
    83  	conn             *grpc.ClientConn
    84  	identityClient   csipbv1.IdentityClient
    85  	controllerClient CSIControllerClient
    86  	nodeClient       CSINodeClient
    87  	logger           hclog.Logger
    88  }
    89  
    90  func (c *client) Close() error {
    91  	if c.conn != nil {
    92  		return c.conn.Close()
    93  	}
    94  	return nil
    95  }
    96  
    97  func NewClient(addr string, logger hclog.Logger) (CSIPlugin, error) {
    98  	if addr == "" {
    99  		return nil, fmt.Errorf("address is empty")
   100  	}
   101  
   102  	conn, err := newGrpcConn(addr, logger)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	return &client{
   108  		conn:             conn,
   109  		identityClient:   csipbv1.NewIdentityClient(conn),
   110  		controllerClient: csipbv1.NewControllerClient(conn),
   111  		nodeClient:       csipbv1.NewNodeClient(conn),
   112  		logger:           logger,
   113  	}, nil
   114  }
   115  
   116  func newGrpcConn(addr string, logger hclog.Logger) (*grpc.ClientConn, error) {
   117  	conn, err := grpc.Dial(
   118  		addr,
   119  		grpc.WithInsecure(),
   120  		grpc.WithUnaryInterceptor(logging.UnaryClientInterceptor(logger)),
   121  		grpc.WithStreamInterceptor(logging.StreamClientInterceptor(logger)),
   122  		grpc.WithDialer(func(target string, timeout time.Duration) (net.Conn, error) {
   123  			return net.DialTimeout("unix", target, timeout)
   124  		}),
   125  	)
   126  
   127  	if err != nil {
   128  		return nil, fmt.Errorf("failed to open grpc connection to addr: %s, err: %v", addr, err)
   129  	}
   130  
   131  	return conn, nil
   132  }
   133  
   134  // PluginInfo describes the type and version of a plugin as required by the nomad
   135  // base.BasePlugin interface.
   136  func (c *client) PluginInfo() (*base.PluginInfoResponse, error) {
   137  	// note: no grpc retries needed here, as this is called in
   138  	// fingerprinting and will get retried by the caller.
   139  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   140  	defer cancel()
   141  	name, version, err := c.PluginGetInfo(ctx)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	return &base.PluginInfoResponse{
   147  		Type:              PluginTypeCSI,     // note: this isn't a Nomad go-plugin type
   148  		PluginApiVersions: []string{"1.0.0"}, // TODO(tgross): we want to fingerprint spec version, but this isn't included as a field from the plugins
   149  		PluginVersion:     version,
   150  		Name:              name,
   151  	}, nil
   152  }
   153  
   154  // ConfigSchema returns the schema for parsing the plugins configuration as
   155  // required by the base.BasePlugin interface. It will always return nil.
   156  func (c *client) ConfigSchema() (*hclspec.Spec, error) {
   157  	return nil, nil
   158  }
   159  
   160  // SetConfig is used to set the configuration by passing a MessagePack
   161  // encoding of it.
   162  func (c *client) SetConfig(_ *base.Config) error {
   163  	return fmt.Errorf("unsupported")
   164  }
   165  
   166  func (c *client) PluginProbe(ctx context.Context) (bool, error) {
   167  	// note: no grpc retries should be done here
   168  	req, err := c.identityClient.Probe(ctx, &csipbv1.ProbeRequest{})
   169  	if err != nil {
   170  		return false, err
   171  	}
   172  
   173  	wrapper := req.GetReady()
   174  
   175  	// wrapper.GetValue() protects against wrapper being `nil`, and returns false.
   176  	ready := wrapper.GetValue()
   177  
   178  	if wrapper == nil {
   179  		// If the plugin returns a nil value for ready, then it should be
   180  		// interpreted as the plugin is ready for compatibility with plugins that
   181  		// do not do health checks.
   182  		ready = true
   183  	}
   184  
   185  	return ready, nil
   186  }
   187  
   188  func (c *client) PluginGetInfo(ctx context.Context) (string, string, error) {
   189  	if c == nil {
   190  		return "", "", fmt.Errorf("Client not initialized")
   191  	}
   192  	if c.identityClient == nil {
   193  		return "", "", fmt.Errorf("Client not initialized")
   194  	}
   195  
   196  	resp, err := c.identityClient.GetPluginInfo(ctx, &csipbv1.GetPluginInfoRequest{})
   197  	if err != nil {
   198  		return "", "", err
   199  	}
   200  
   201  	name := resp.GetName()
   202  	if name == "" {
   203  		return "", "", fmt.Errorf("PluginGetInfo: plugin returned empty name field")
   204  	}
   205  	version := resp.GetVendorVersion()
   206  
   207  	return name, version, nil
   208  }
   209  
   210  func (c *client) PluginGetCapabilities(ctx context.Context) (*PluginCapabilitySet, error) {
   211  	if c == nil {
   212  		return nil, fmt.Errorf("Client not initialized")
   213  	}
   214  	if c.identityClient == nil {
   215  		return nil, fmt.Errorf("Client not initialized")
   216  	}
   217  
   218  	// note: no grpc retries needed here, as this is called in
   219  	// fingerprinting and will get retried by the caller
   220  	resp, err := c.identityClient.GetPluginCapabilities(ctx,
   221  		&csipbv1.GetPluginCapabilitiesRequest{})
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	return NewPluginCapabilitySet(resp), nil
   227  }
   228  
   229  //
   230  // Controller Endpoints
   231  //
   232  
   233  func (c *client) ControllerGetCapabilities(ctx context.Context) (*ControllerCapabilitySet, error) {
   234  	if c == nil {
   235  		return nil, fmt.Errorf("Client not initialized")
   236  	}
   237  	if c.controllerClient == nil {
   238  		return nil, fmt.Errorf("controllerClient not initialized")
   239  	}
   240  
   241  	// note: no grpc retries needed here, as this is called in
   242  	// fingerprinting and will get retried by the caller
   243  	resp, err := c.controllerClient.ControllerGetCapabilities(ctx,
   244  		&csipbv1.ControllerGetCapabilitiesRequest{})
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  
   249  	return NewControllerCapabilitySet(resp), nil
   250  }
   251  
   252  func (c *client) ControllerPublishVolume(ctx context.Context, req *ControllerPublishVolumeRequest, opts ...grpc.CallOption) (*ControllerPublishVolumeResponse, error) {
   253  	if c == nil {
   254  		return nil, fmt.Errorf("Client not initialized")
   255  	}
   256  	if c.controllerClient == nil {
   257  		return nil, fmt.Errorf("controllerClient not initialized")
   258  	}
   259  
   260  	err := req.Validate()
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  
   265  	pbrequest := req.ToCSIRepresentation()
   266  	resp, err := c.controllerClient.ControllerPublishVolume(ctx, pbrequest, opts...)
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  
   271  	return &ControllerPublishVolumeResponse{
   272  		PublishContext: helper.CopyMapStringString(resp.PublishContext),
   273  	}, nil
   274  }
   275  
   276  func (c *client) ControllerUnpublishVolume(ctx context.Context, req *ControllerUnpublishVolumeRequest, opts ...grpc.CallOption) (*ControllerUnpublishVolumeResponse, error) {
   277  	if c == nil {
   278  		return nil, fmt.Errorf("Client not initialized")
   279  	}
   280  	if c.controllerClient == nil {
   281  		return nil, fmt.Errorf("controllerClient not initialized")
   282  	}
   283  	err := req.Validate()
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  
   288  	upbrequest := req.ToCSIRepresentation()
   289  	_, err = c.controllerClient.ControllerUnpublishVolume(ctx, upbrequest, opts...)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  
   294  	return &ControllerUnpublishVolumeResponse{}, nil
   295  }
   296  
   297  func (c *client) ControllerValidateCapabilities(ctx context.Context, volumeID string, capabilities *VolumeCapability, secrets structs.CSISecrets, opts ...grpc.CallOption) error {
   298  	if c == nil {
   299  		return fmt.Errorf("Client not initialized")
   300  	}
   301  	if c.controllerClient == nil {
   302  		return fmt.Errorf("controllerClient not initialized")
   303  	}
   304  
   305  	if volumeID == "" {
   306  		return fmt.Errorf("missing VolumeID")
   307  	}
   308  
   309  	if capabilities == nil {
   310  		return fmt.Errorf("missing Capabilities")
   311  	}
   312  
   313  	req := &csipbv1.ValidateVolumeCapabilitiesRequest{
   314  		VolumeId: volumeID,
   315  		VolumeCapabilities: []*csipbv1.VolumeCapability{
   316  			capabilities.ToCSIRepresentation(),
   317  		},
   318  		// VolumeContext: map[string]string // TODO: https://github.com/hashicorp/nomad/issues/7771
   319  		// Parameters: map[string]string // TODO: https://github.com/hashicorp/nomad/issues/7670
   320  		Secrets: secrets,
   321  	}
   322  
   323  	resp, err := c.controllerClient.ValidateVolumeCapabilities(ctx, req, opts...)
   324  	if err != nil {
   325  		return err
   326  	}
   327  
   328  	if resp.Message != "" {
   329  		// this should only ever be set if Confirmed isn't set, but
   330  		// it's not a validation failure.
   331  		c.logger.Debug(resp.Message)
   332  	}
   333  
   334  	// The protobuf accessors below safely handle nil pointers.
   335  	// The CSI spec says we can only assert the plugin has
   336  	// confirmed the volume capabilities, not that it hasn't
   337  	// confirmed them, so if the field is nil we have to assume
   338  	// the volume is ok.
   339  	confirmedCaps := resp.GetConfirmed().GetVolumeCapabilities()
   340  	if confirmedCaps != nil {
   341  		for _, requestedCap := range req.VolumeCapabilities {
   342  			err := compareCapabilities(requestedCap, confirmedCaps)
   343  			if err != nil {
   344  				return fmt.Errorf("volume capability validation failed: %v", err)
   345  			}
   346  		}
   347  	}
   348  
   349  	return nil
   350  }
   351  
   352  // compareCapabilities returns an error if the 'got' capabilities does not
   353  // contain the 'expected' capability
   354  func compareCapabilities(expected *csipbv1.VolumeCapability, got []*csipbv1.VolumeCapability) error {
   355  	var err multierror.Error
   356  	for _, cap := range got {
   357  
   358  		expectedMode := expected.GetAccessMode().GetMode()
   359  		capMode := cap.GetAccessMode().GetMode()
   360  
   361  		if expectedMode != capMode {
   362  			multierror.Append(&err,
   363  				fmt.Errorf("requested AccessMode %v, got %v", expectedMode, capMode))
   364  			continue
   365  		}
   366  
   367  		// AccessType Block is an empty struct even if set, so the
   368  		// only way to test for it is to check that the AccessType
   369  		// isn't Mount.
   370  		expectedMount := expected.GetMount()
   371  		capMount := cap.GetMount()
   372  
   373  		if expectedMount == nil {
   374  			if capMount == nil {
   375  				return nil
   376  			}
   377  			multierror.Append(&err, fmt.Errorf(
   378  				"requested AccessType Block but got AccessType Mount"))
   379  			continue
   380  		}
   381  
   382  		if capMount == nil {
   383  			multierror.Append(&err, fmt.Errorf(
   384  				"requested AccessType Mount but got AccessType Block"))
   385  			continue
   386  		}
   387  
   388  		if expectedMount.FsType != capMount.FsType {
   389  			multierror.Append(&err, fmt.Errorf(
   390  				"requested AccessType mount filesystem type %v, got %v",
   391  				expectedMount.FsType, capMount.FsType))
   392  			continue
   393  		}
   394  
   395  		for _, expectedFlag := range expectedMount.MountFlags {
   396  			var ok bool
   397  			for _, flag := range capMount.MountFlags {
   398  				if expectedFlag == flag {
   399  					ok = true
   400  					break
   401  				}
   402  			}
   403  			if !ok {
   404  				// mount flags can contain sensitive data, so we can't log details
   405  				multierror.Append(&err, fmt.Errorf(
   406  					"requested mount flags did not match available capabilities"))
   407  				continue
   408  			}
   409  		}
   410  		return nil
   411  	}
   412  	return err.ErrorOrNil()
   413  }
   414  
   415  //
   416  // Node Endpoints
   417  //
   418  
   419  func (c *client) NodeGetCapabilities(ctx context.Context) (*NodeCapabilitySet, error) {
   420  	if c == nil {
   421  		return nil, fmt.Errorf("Client not initialized")
   422  	}
   423  	if c.nodeClient == nil {
   424  		return nil, fmt.Errorf("Client not initialized")
   425  	}
   426  
   427  	// note: no grpc retries needed here, as this is called in
   428  	// fingerprinting and will get retried by the caller
   429  	resp, err := c.nodeClient.NodeGetCapabilities(ctx, &csipbv1.NodeGetCapabilitiesRequest{})
   430  	if err != nil {
   431  		return nil, err
   432  	}
   433  
   434  	return NewNodeCapabilitySet(resp), nil
   435  }
   436  
   437  func (c *client) NodeGetInfo(ctx context.Context) (*NodeGetInfoResponse, error) {
   438  	if c == nil {
   439  		return nil, fmt.Errorf("Client not initialized")
   440  	}
   441  	if c.nodeClient == nil {
   442  		return nil, fmt.Errorf("Client not initialized")
   443  	}
   444  
   445  	result := &NodeGetInfoResponse{}
   446  
   447  	// note: no grpc retries needed here, as this is called in
   448  	// fingerprinting and will get retried by the caller
   449  	resp, err := c.nodeClient.NodeGetInfo(ctx, &csipbv1.NodeGetInfoRequest{})
   450  	if err != nil {
   451  		return nil, err
   452  	}
   453  
   454  	if resp.GetNodeId() == "" {
   455  		return nil, fmt.Errorf("plugin failed to return nodeid")
   456  	}
   457  
   458  	result.NodeID = resp.GetNodeId()
   459  	result.MaxVolumes = resp.GetMaxVolumesPerNode()
   460  	if result.MaxVolumes == 0 {
   461  		// set safe default so that scheduler ignores this constraint when not set
   462  		result.MaxVolumes = math.MaxInt64
   463  	}
   464  
   465  	return result, nil
   466  }
   467  
   468  func (c *client) NodeStageVolume(ctx context.Context, volumeID string, publishContext map[string]string, stagingTargetPath string, capabilities *VolumeCapability, secrets structs.CSISecrets, opts ...grpc.CallOption) error {
   469  	if c == nil {
   470  		return fmt.Errorf("Client not initialized")
   471  	}
   472  	if c.nodeClient == nil {
   473  		return fmt.Errorf("Client not initialized")
   474  	}
   475  
   476  	// These errors should not be returned during production use but exist as aids
   477  	// during Nomad Development
   478  	if volumeID == "" {
   479  		return fmt.Errorf("missing volumeID")
   480  	}
   481  	if stagingTargetPath == "" {
   482  		return fmt.Errorf("missing stagingTargetPath")
   483  	}
   484  
   485  	req := &csipbv1.NodeStageVolumeRequest{
   486  		VolumeId:          volumeID,
   487  		PublishContext:    publishContext,
   488  		StagingTargetPath: stagingTargetPath,
   489  		VolumeCapability:  capabilities.ToCSIRepresentation(),
   490  		Secrets:           secrets,
   491  	}
   492  
   493  	// NodeStageVolume's response contains no extra data. If err == nil, we were
   494  	// successful.
   495  	_, err := c.nodeClient.NodeStageVolume(ctx, req, opts...)
   496  	return err
   497  }
   498  
   499  func (c *client) NodeUnstageVolume(ctx context.Context, volumeID string, stagingTargetPath string, opts ...grpc.CallOption) error {
   500  	if c == nil {
   501  		return fmt.Errorf("Client not initialized")
   502  	}
   503  	if c.nodeClient == nil {
   504  		return fmt.Errorf("Client not initialized")
   505  	}
   506  	// These errors should not be returned during production use but exist as aids
   507  	// during Nomad Development
   508  	if volumeID == "" {
   509  		return fmt.Errorf("missing volumeID")
   510  	}
   511  	if stagingTargetPath == "" {
   512  		return fmt.Errorf("missing stagingTargetPath")
   513  	}
   514  
   515  	req := &csipbv1.NodeUnstageVolumeRequest{
   516  		VolumeId:          volumeID,
   517  		StagingTargetPath: stagingTargetPath,
   518  	}
   519  
   520  	// NodeUnstageVolume's response contains no extra data. If err == nil, we were
   521  	// successful.
   522  	_, err := c.nodeClient.NodeUnstageVolume(ctx, req, opts...)
   523  	return err
   524  }
   525  
   526  func (c *client) NodePublishVolume(ctx context.Context, req *NodePublishVolumeRequest, opts ...grpc.CallOption) error {
   527  	if c == nil {
   528  		return fmt.Errorf("Client not initialized")
   529  	}
   530  	if c.nodeClient == nil {
   531  		return fmt.Errorf("Client not initialized")
   532  	}
   533  
   534  	if err := req.Validate(); err != nil {
   535  		return fmt.Errorf("validation error: %v", err)
   536  	}
   537  
   538  	// NodePublishVolume's response contains no extra data. If err == nil, we were
   539  	// successful.
   540  	_, err := c.nodeClient.NodePublishVolume(ctx, req.ToCSIRepresentation(), opts...)
   541  	return err
   542  }
   543  
   544  func (c *client) NodeUnpublishVolume(ctx context.Context, volumeID, targetPath string, opts ...grpc.CallOption) error {
   545  	if c == nil {
   546  		return fmt.Errorf("Client not initialized")
   547  	}
   548  	if c.nodeClient == nil {
   549  		return fmt.Errorf("Client not initialized")
   550  	}
   551  
   552  	if volumeID == "" {
   553  		return fmt.Errorf("missing VolumeID")
   554  	}
   555  
   556  	if targetPath == "" {
   557  		return fmt.Errorf("missing TargetPath")
   558  	}
   559  
   560  	req := &csipbv1.NodeUnpublishVolumeRequest{
   561  		VolumeId:   volumeID,
   562  		TargetPath: targetPath,
   563  	}
   564  
   565  	// NodeUnpublishVolume's response contains no extra data. If err == nil, we were
   566  	// successful.
   567  	_, err := c.nodeClient.NodeUnpublishVolume(ctx, req, opts...)
   568  	return err
   569  }