2020-02-21 10:32:10 +00:00
|
|
|
package nomad
|
|
|
|
|
|
|
|
import (
|
2020-03-10 14:22:42 +00:00
|
|
|
"fmt"
|
2020-04-02 20:04:56 +00:00
|
|
|
"math/rand"
|
2020-08-11 14:25:43 +00:00
|
|
|
"strings"
|
2020-02-21 10:32:10 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
metrics "github.com/armon/go-metrics"
|
|
|
|
log "github.com/hashicorp/go-hclog"
|
2020-04-02 20:04:56 +00:00
|
|
|
memdb "github.com/hashicorp/go-memdb"
|
2020-02-21 10:32:10 +00:00
|
|
|
cstructs "github.com/hashicorp/nomad/client/structs"
|
|
|
|
)
|
|
|
|
|
2020-04-02 20:04:56 +00:00
|
|
|
// ClientCSI is used to forward RPC requests to the targed Nomad client's
|
2020-02-21 10:32:10 +00:00
|
|
|
// CSIController endpoint.
|
2020-04-02 20:04:56 +00:00
|
|
|
type ClientCSI struct {
|
2020-02-21 10:32:10 +00:00
|
|
|
srv *Server
|
|
|
|
logger log.Logger
|
|
|
|
}
|
|
|
|
|
2020-04-02 20:04:56 +00:00
|
|
|
func (a *ClientCSI) ControllerAttachVolume(args *cstructs.ClientCSIControllerAttachVolumeRequest, reply *cstructs.ClientCSIControllerAttachVolumeResponse) error {
|
2020-02-21 10:32:10 +00:00
|
|
|
defer metrics.MeasureSince([]string{"nomad", "client_csi_controller", "attach_volume"}, time.Now())
|
2020-08-06 17:24:24 +00:00
|
|
|
|
|
|
|
clientIDs, err := a.clientIDsForController(args.PluginID)
|
2020-02-21 10:32:10 +00:00
|
|
|
if err != nil {
|
2020-08-06 17:24:24 +00:00
|
|
|
return fmt.Errorf("controller attach volume: %v", err)
|
2020-02-21 10:32:10 +00:00
|
|
|
}
|
|
|
|
|
2020-08-06 17:24:24 +00:00
|
|
|
for _, clientID := range clientIDs {
|
|
|
|
args.ControllerNodeID = clientID
|
|
|
|
state, ok := a.srv.getNodeConn(clientID)
|
|
|
|
if !ok {
|
|
|
|
return findNodeConnAndForward(a.srv,
|
|
|
|
clientID, "ClientCSI.ControllerAttachVolume", args, reply)
|
|
|
|
}
|
2020-02-21 10:32:10 +00:00
|
|
|
|
2020-08-06 17:24:24 +00:00
|
|
|
err = NodeRpc(state.Session, "CSI.ControllerAttachVolume", args, reply)
|
|
|
|
if err == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2020-08-11 13:13:52 +00:00
|
|
|
if a.isRetryable(err) {
|
2020-08-11 14:25:43 +00:00
|
|
|
a.logger.Debug("failed to reach controller on client",
|
|
|
|
"nodeID", clientID, "err", err)
|
2020-08-06 17:24:24 +00:00
|
|
|
continue
|
|
|
|
}
|
2020-04-02 20:04:56 +00:00
|
|
|
return fmt.Errorf("controller attach volume: %v", err)
|
2020-03-10 14:22:42 +00:00
|
|
|
}
|
2020-08-06 17:24:24 +00:00
|
|
|
return fmt.Errorf("controller attach volume: %v", err)
|
2020-02-21 10:32:10 +00:00
|
|
|
}
|
|
|
|
|
2020-04-02 20:04:56 +00:00
|
|
|
func (a *ClientCSI) ControllerValidateVolume(args *cstructs.ClientCSIControllerValidateVolumeRequest, reply *cstructs.ClientCSIControllerValidateVolumeResponse) error {
|
2020-02-21 10:32:10 +00:00
|
|
|
defer metrics.MeasureSince([]string{"nomad", "client_csi_controller", "validate_volume"}, time.Now())
|
|
|
|
|
2020-08-06 17:24:24 +00:00
|
|
|
clientIDs, err := a.clientIDsForController(args.PluginID)
|
2020-02-21 10:32:10 +00:00
|
|
|
if err != nil {
|
2020-08-06 17:24:24 +00:00
|
|
|
return fmt.Errorf("validate volume: %v", err)
|
2020-02-21 10:32:10 +00:00
|
|
|
}
|
|
|
|
|
2020-08-06 17:24:24 +00:00
|
|
|
for _, clientID := range clientIDs {
|
|
|
|
args.ControllerNodeID = clientID
|
|
|
|
state, ok := a.srv.getNodeConn(clientID)
|
|
|
|
if !ok {
|
|
|
|
return findNodeConnAndForward(a.srv,
|
|
|
|
clientID, "ClientCSI.ControllerValidateVolume", args, reply)
|
|
|
|
}
|
2020-02-21 10:32:10 +00:00
|
|
|
|
2020-08-06 17:24:24 +00:00
|
|
|
err = NodeRpc(state.Session, "CSI.ControllerValidateVolume", args, reply)
|
|
|
|
if err == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2020-08-11 13:13:52 +00:00
|
|
|
if a.isRetryable(err) {
|
2020-08-11 14:25:43 +00:00
|
|
|
a.logger.Debug("failed to reach controller on client",
|
|
|
|
"nodeID", clientID, "err", err)
|
2020-08-06 17:24:24 +00:00
|
|
|
continue
|
|
|
|
}
|
2020-03-10 14:22:42 +00:00
|
|
|
return fmt.Errorf("validate volume: %v", err)
|
|
|
|
}
|
2020-08-06 17:24:24 +00:00
|
|
|
return fmt.Errorf("validate volume: %v", err)
|
2020-02-21 10:32:10 +00:00
|
|
|
}
|
2020-03-16 19:59:42 +00:00
|
|
|
|
2020-04-02 20:04:56 +00:00
|
|
|
func (a *ClientCSI) ControllerDetachVolume(args *cstructs.ClientCSIControllerDetachVolumeRequest, reply *cstructs.ClientCSIControllerDetachVolumeResponse) error {
|
2020-03-16 19:59:42 +00:00
|
|
|
defer metrics.MeasureSince([]string{"nomad", "client_csi_controller", "detach_volume"}, time.Now())
|
|
|
|
|
2020-08-06 17:24:24 +00:00
|
|
|
clientIDs, err := a.clientIDsForController(args.PluginID)
|
2020-03-16 19:59:42 +00:00
|
|
|
if err != nil {
|
2020-08-06 17:24:24 +00:00
|
|
|
return fmt.Errorf("controller detach volume: %v", err)
|
2020-03-16 19:59:42 +00:00
|
|
|
}
|
|
|
|
|
2020-08-06 17:24:24 +00:00
|
|
|
for _, clientID := range clientIDs {
|
|
|
|
args.ControllerNodeID = clientID
|
|
|
|
state, ok := a.srv.getNodeConn(clientID)
|
|
|
|
if !ok {
|
|
|
|
return findNodeConnAndForward(a.srv,
|
|
|
|
clientID, "ClientCSI.ControllerDetachVolume", args, reply)
|
|
|
|
}
|
2020-03-16 19:59:42 +00:00
|
|
|
|
2020-08-06 17:24:24 +00:00
|
|
|
err = NodeRpc(state.Session, "CSI.ControllerDetachVolume", args, reply)
|
|
|
|
if err == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2020-08-11 13:13:52 +00:00
|
|
|
if a.isRetryable(err) {
|
2020-08-11 14:25:43 +00:00
|
|
|
a.logger.Debug("failed to reach controller on client",
|
|
|
|
"nodeID", clientID, "err", err)
|
2020-08-06 17:24:24 +00:00
|
|
|
continue
|
|
|
|
}
|
2020-04-02 20:04:56 +00:00
|
|
|
return fmt.Errorf("controller detach volume: %v", err)
|
2020-03-16 19:59:42 +00:00
|
|
|
}
|
2020-08-06 17:24:24 +00:00
|
|
|
return fmt.Errorf("controller detach volume: %v", err)
|
|
|
|
}
|
2020-03-16 19:59:42 +00:00
|
|
|
|
2020-08-06 17:24:24 +00:00
|
|
|
// we can retry the same RPC on a different controller in the cases where the
|
|
|
|
// client has stopped and been GC'd, or where the controller has stopped but
|
|
|
|
// we don't have the fingerprint update yet
|
2020-08-11 13:13:52 +00:00
|
|
|
func (a *ClientCSI) isRetryable(err error) bool {
|
2020-08-11 14:25:43 +00:00
|
|
|
// TODO: msgpack-rpc mangles the error so we lose the wrapping,
|
|
|
|
// but if that can be fixed upstream we should use that here instead
|
|
|
|
return strings.Contains(err.Error(), "CSI client error (retryable)") ||
|
|
|
|
strings.Contains(err.Error(), "Unknown node")
|
2020-03-16 19:59:42 +00:00
|
|
|
}
|
2020-04-02 20:04:56 +00:00
|
|
|
|
|
|
|
func (a *ClientCSI) NodeDetachVolume(args *cstructs.ClientCSINodeDetachVolumeRequest, reply *cstructs.ClientCSINodeDetachVolumeResponse) error {
|
|
|
|
defer metrics.MeasureSince([]string{"nomad", "client_csi_node", "detach_volume"}, time.Now())
|
|
|
|
|
|
|
|
// Make sure Node is valid and new enough to support RPC
|
|
|
|
snap, err := a.srv.State().Snapshot()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = getNodeForRpc(snap, args.NodeID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the connection to the client
|
|
|
|
state, ok := a.srv.getNodeConn(args.NodeID)
|
|
|
|
if !ok {
|
|
|
|
return findNodeConnAndForward(a.srv, args.NodeID, "ClientCSI.NodeDetachVolume", args, reply)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make the RPC
|
|
|
|
err = NodeRpc(state.Session, "CSI.NodeDetachVolume", args, reply)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("node detach volume: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-08-06 17:24:24 +00:00
|
|
|
// clientIDsForController returns a shuffled list of client IDs where the
|
|
|
|
// controller plugin is expected to be running.
|
|
|
|
func (a *ClientCSI) clientIDsForController(pluginID string) ([]string, error) {
|
2020-04-10 20:47:21 +00:00
|
|
|
|
|
|
|
snap, err := a.srv.State().Snapshot()
|
|
|
|
if err != nil {
|
2020-08-06 17:24:24 +00:00
|
|
|
return nil, err
|
2020-04-10 20:47:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if pluginID == "" {
|
2020-08-06 17:24:24 +00:00
|
|
|
return nil, fmt.Errorf("missing plugin ID")
|
2020-04-10 20:47:21 +00:00
|
|
|
}
|
2020-05-10 01:35:42 +00:00
|
|
|
|
2020-04-10 20:47:21 +00:00
|
|
|
ws := memdb.NewWatchSet()
|
|
|
|
|
|
|
|
// note: plugin IDs are not scoped to region/DC but volumes are.
|
|
|
|
// so any node we get for a controller is already in the same
|
|
|
|
// region/DC for the volume.
|
|
|
|
plugin, err := snap.CSIPluginByID(ws, pluginID)
|
|
|
|
if err != nil {
|
2020-08-06 17:24:24 +00:00
|
|
|
return nil, fmt.Errorf("error getting plugin: %s, %v", pluginID, err)
|
2020-04-10 20:47:21 +00:00
|
|
|
}
|
|
|
|
if plugin == nil {
|
2020-08-06 17:24:24 +00:00
|
|
|
return nil, fmt.Errorf("plugin missing: %s", pluginID)
|
2020-04-02 20:04:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// iterating maps is "random" but unspecified and isn't particularly
|
|
|
|
// random with small maps, so not well-suited for load balancing.
|
|
|
|
// so we shuffle the keys and iterate over them.
|
2020-08-06 17:24:24 +00:00
|
|
|
clientIDs := []string{}
|
2020-04-02 20:04:56 +00:00
|
|
|
|
2020-08-06 17:24:24 +00:00
|
|
|
for clientID, controller := range plugin.Controllers {
|
2020-04-02 20:04:56 +00:00
|
|
|
if !controller.IsController() {
|
|
|
|
// we don't have separate types for CSIInfo depending on
|
|
|
|
// whether it's a controller or node. this error shouldn't
|
|
|
|
// make it to production but is to aid developers during
|
|
|
|
// development
|
|
|
|
continue
|
|
|
|
}
|
2020-08-06 17:24:24 +00:00
|
|
|
node, err := getNodeForRpc(snap, clientID)
|
|
|
|
if err == nil && node != nil && node.Ready() {
|
|
|
|
clientIDs = append(clientIDs, clientID)
|
2020-04-02 20:04:56 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-06 17:24:24 +00:00
|
|
|
if len(clientIDs) == 0 {
|
|
|
|
return nil, fmt.Errorf("failed to find clients running controller plugin %q", pluginID)
|
|
|
|
}
|
|
|
|
|
|
|
|
rand.Shuffle(len(clientIDs), func(i, j int) {
|
|
|
|
clientIDs[i], clientIDs[j] = clientIDs[j], clientIDs[i]
|
|
|
|
})
|
2020-04-02 20:04:56 +00:00
|
|
|
|
2020-08-06 17:24:24 +00:00
|
|
|
return clientIDs, nil
|
2020-04-02 20:04:56 +00:00
|
|
|
}
|