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 }