proxy: add NodeGetInfo support
This commit is contained in:
		
							parent
							
								
									7aeac628b8
								
							
						
					
					
						commit
						279c241fd1
					
				|  | @ -6,10 +6,11 @@ const fs = require('fs'); | |||
| const { Registry } = require("../../utils/registry"); | ||||
| const { GrpcError, grpc } = require("../../utils/grpc"); | ||||
| const path = require('path'); | ||||
| const os = require("os"); | ||||
| 
 | ||||
| const volumeIdPrefix = 'v:'; | ||||
| const snapshotIdPrefix = 's:'; | ||||
| const NODE_TOPOLOGY_KEY_NAME = "org.democratic-csi.topology/node"; | ||||
| const TOPOLOGY_DEFAULT_PREFIX = 'org.democratic-csi.topology'; | ||||
| 
 | ||||
| class CsiProxyDriver extends CsiBaseDriver { | ||||
|   constructor(ctx, options) { | ||||
|  | @ -21,6 +22,7 @@ class CsiProxyDriver extends CsiBaseDriver { | |||
|     if (configFolder.slice(-1) == '/') { | ||||
|       configFolder = configFolder.slice(0, -1); | ||||
|     } | ||||
|     this.nodeIdSerializer = new NodeIdSerializer(ctx, options.proxy.nodeId || {}); | ||||
| 
 | ||||
|     const timeoutMinutes = this.options.proxy.cacheTimeoutMinutes ?? 60; | ||||
|     const defaultOptions = this.options; | ||||
|  | @ -225,7 +227,6 @@ class CsiProxyDriver extends CsiBaseDriver { | |||
|         ); | ||||
|     } | ||||
|     const result = await this.checkAndRun(driver, 'CreateVolume', call); | ||||
|     this.ctx.logger.debug("CreateVolume result " + result); | ||||
|     result.volume.volume_id = this.decorateVolumeHandle(connectionName, result.volume.volume_id); | ||||
|     return result; | ||||
|   } | ||||
|  | @ -292,9 +293,8 @@ class CsiProxyDriver extends CsiBaseDriver { | |||
|   } | ||||
| 
 | ||||
|   async NodeGetInfo(call) { | ||||
|     const nodeName = process.env.CSI_NODE_ID || os.hostname(); | ||||
|     const result = { | ||||
|       node_id: nodeName, | ||||
|       node_id: this.nodeIdSerializer.serialize(), | ||||
|       max_volumes_per_node: 0, | ||||
|     }; | ||||
|     const topologyType = this.options.proxy.nodeTopology?.type ?? 'cluster'; | ||||
|  | @ -310,7 +310,7 @@ class CsiProxyDriver extends CsiBaseDriver { | |||
|       case 'node': | ||||
|         result.accessible_topology = { | ||||
|           segments: { | ||||
|             [prefix + '/node']: nodeName, | ||||
|             [prefix + '/node']: NodeIdSerializer.getLocalNodeName(), | ||||
|           }, | ||||
|         }; | ||||
|         break | ||||
|  | @ -507,4 +507,124 @@ class DriverCache { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| const nodeIdCode_NodeName = 'n'; | ||||
| const nodeIdCode_Hostname = 'h'; | ||||
| const nodeIdCode_ISCSI = 'i'; | ||||
| const nodeIdCode_NVMEOF = 'v'; | ||||
| class NodeIdSerializer { | ||||
|   constructor(ctx, nodeIdConfig) { | ||||
|     this.ctx = ctx; | ||||
|     this.config = nodeIdConfig; | ||||
|   } | ||||
| 
 | ||||
|   static getLocalNodeName() { | ||||
|     if (!process.env.CSI_NODE_ID) { | ||||
|       throw 'CSI_NODE_ID is required for proxy driver'; | ||||
|     } | ||||
|     return process.env.CSI_NODE_ID; | ||||
|   } | ||||
|   static getLocalIqn() { | ||||
|     const iqnPath = '/etc/iscsi/initiatorname.iscsi'; | ||||
|     const lines = fs.readFileSync(iqnPath, "utf8").split('\n'); | ||||
|     for (let line of lines) { | ||||
|       line = line.replace(/#.*/, '').replace(/\s+$/, ''); | ||||
|       if (line == '') { | ||||
|         continue; | ||||
|       } | ||||
|       const linePrefix = 'InitiatorName='; | ||||
|       if (line.startsWith(linePrefix)) { | ||||
|         const iqn = line.slice(linePrefix.length); | ||||
|         return iqn; | ||||
|       } | ||||
|     } | ||||
|     throw 'iqn is not found'; | ||||
|   } | ||||
|   static getLocalNqn() { | ||||
|     const nqnPath = '/etc/nvme/hostnqn'; | ||||
|     return fs.readFileSync(nqnPath, "utf8").replace(/\s+$/, ''); | ||||
|   } | ||||
| 
 | ||||
|   // returns { prefixName, suffix }
 | ||||
|   findPrefix(value, prefixMap) { | ||||
|     for (const prefixInfo of prefixMap) { | ||||
|       if (value.startsWith(prefixInfo.prefix)) { | ||||
|         return { | ||||
|           prefixName: prefixInfo.shortName, | ||||
|           suffix: value.split(prefixInfo.prefix.length), | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   serializeByPrefix(code, value, prefixMap) { | ||||
|     const prefixInfo = prefixMap.find(prefixInfo => value.startsWith(prefixInfo.prefix)); | ||||
|     if (!prefixInfo) { | ||||
|       throw `node id: prefix is not found for value: ${value}`; | ||||
|     } | ||||
|     if (!prefixInfo.shortName.match(/^[0-9a-z]+$/)) { | ||||
|       throw `prefix short name must be alphanumeric, invalid name: '${prefixInfo.shortName}'`; | ||||
|     } | ||||
|     const suffix = value.substring(prefixInfo.prefix.length); | ||||
|     return code + prefixInfo.shortName + '=' + suffix; | ||||
|   } | ||||
| 
 | ||||
|   deserializeFromPrefix(value, prefixMap, humanName) { | ||||
|     const prefixName = value.substring(0, value.indexOf('=')); | ||||
|     const suffix = value.substring(value.indexOf('=') + 1); | ||||
|     const prefixInfo = prefixMap.find(prefixInfo => prefixInfo.shortName === prefixName); | ||||
|     if (!prefixInfo) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         `unknown node prefix short name for ${humanName}: ${value}` | ||||
|       ); | ||||
|     } | ||||
|     return prefixInfo.prefix + suffix; | ||||
|   } | ||||
| 
 | ||||
|   // returns a single string that incorporates node id components specified in config.parts
 | ||||
|   serialize() { | ||||
|     let result = ''; | ||||
|     if (this.config.parts?.nodeName ?? true) { | ||||
|       result += '/' + nodeIdCode_NodeName + '=' + NodeIdSerializer.getLocalNodeName(); | ||||
|     } | ||||
|     if (this.config.parts?.hostname ?? false) { | ||||
|       result += '/' + nodeIdCode_Hostname + '=' + os.hostname(); | ||||
|     } | ||||
|     if (this.config.parts?.iqn ?? false) { | ||||
|       result += '/' + this.serializeByPrefix(nodeIdCode_ISCSI, NodeIdSerializer.getLocalIqn(), this.config.iqnPrefix); | ||||
|     } | ||||
|     if (this.config.parts?.nqn ?? false) { | ||||
|       result += '/' + this.serializeByPrefix(nodeIdCode_NVMEOF, NodeIdSerializer.getLocalNqn(), this.config.nqnPrefix); | ||||
|     } | ||||
|     if (result === '') { | ||||
|       throw 'node id can not be empty'; | ||||
|     } | ||||
|     // remove starting /
 | ||||
|     return result.slice(1); | ||||
|   } | ||||
| 
 | ||||
|   // takes a string generated by NodeIdSerializer.serialize
 | ||||
|   // returns an { nodeName, iqn, nqn } if they exist in nodeId
 | ||||
|   deserialize(nodeId) { | ||||
|     const result = {}; | ||||
|     for (const v in nodeId.split("/")) { | ||||
|       switch (v[0]) { | ||||
|         case nodeIdCode_NodeName: | ||||
|           result.nodeName = v.substring(v.indexOf('=') + 1); | ||||
|           continue; | ||||
|         case nodeIdCode_Hostname: | ||||
|           result.hostname = v.substring(v.indexOf('=') + 1); | ||||
|           continue; | ||||
|         case nodeIdCode_ISCSI: | ||||
|           result.iqn = this.deserializeFromPrefix(v, this.config.iqnPrefix, 'iSCSI'); | ||||
|           continue; | ||||
|         case nodeIdCode_NVMEOF: | ||||
|           result.nqn = this.deserializeFromPrefix(v, this.config.nqnPrefix, 'NVMEoF'); | ||||
|           continue; | ||||
|       } | ||||
|     } | ||||
|     return result; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| module.exports.CsiProxyDriver = CsiProxyDriver; | ||||
|  |  | |||
|  | @ -8,12 +8,11 @@ There are 2 important values: | |||
| - topology | ||||
| - node ID | ||||
| 
 | ||||
| There are only 2 types of topology in democratic-csi: | ||||
| topology without constraints and node-local volumes. | ||||
| It's easy to account for with proxy settings. | ||||
| # Node ID | ||||
| 
 | ||||
| Node ID is a bit harder to solve, but this page suggests a solution. | ||||
| Also, currently no real driver actually needs `node_id` to work, | ||||
| Node ID is a bit tricky to solve, because of limited field length. | ||||
| 
 | ||||
| Currently no real driver actually needs `node_id` to work, | ||||
| so all of this is mostly a proof-of-concept. | ||||
| A proof that you can create a functional proxy driver even with current CSI spec. | ||||
| 
 | ||||
|  | @ -23,7 +22,7 @@ before calling the actual real driver method. | |||
| Node ID docs are not a part of user documentation because currently this is very theoretical. | ||||
| Current implementation works fine but doesn't do anything useful for users. | ||||
| 
 | ||||
| # Node info: config example | ||||
| ## Node ID: config example | ||||
| 
 | ||||
| ```yaml | ||||
| # configured in root proxy config | ||||
|  | @ -62,7 +61,7 @@ proxy: | |||
|     nodeIdType: nodeName | ||||
| ``` | ||||
| 
 | ||||
| # Reasoning why such complex node_id is required | ||||
| ## Node ID: Reasoning why such complex node_id is required | ||||
| 
 | ||||
| `node_name + iqn + nqn` can be very long. | ||||
| 
 | ||||
|  | @ -89,7 +88,7 @@ For example, if driver needs iqn, proxy will find field in node id starting with | |||
| search `proxy.nodeId.iqnPrefix` for entry with `shortName = 1`, and then set `node_id` to | ||||
| `proxy.nodeId.iqnPrefix[name=1].prefix` + `qwerty` | ||||
| 
 | ||||
| ## Alternatives to prefixes | ||||
| ## Node ID: Alternatives to prefixes | ||||
| 
 | ||||
| Each driver can override `node_id` based on node name. | ||||
| 
 | ||||
|  | @ -118,3 +117,28 @@ Still, if this were to be useful for some reason, this is fully compatible with | |||
| 
 | ||||
| Theoretically, more info can be extracted from node to be used in `nodeIdTemplate`, | ||||
| provided the info is short enough to fit into `node_id` length limit. | ||||
| 
 | ||||
| # Topology | ||||
| 
 | ||||
| There are 3 cases of cluster topology: | ||||
| 
 | ||||
| - Each node has unique topology domain (`local` drivers) | ||||
| - All nodes are the same (usually the case for non-local drivers) | ||||
| - Several availability zones that can contain several nodes | ||||
| 
 | ||||
| Simple cases are currently supported by the proxy. | ||||
| Custom availability zones are TBD. | ||||
| 
 | ||||
| Example configuration: | ||||
| 
 | ||||
| ```yaml | ||||
| proxy: | ||||
|   nodeTopology: | ||||
|     # allowed values: | ||||
|     # node - each node has its own storage | ||||
|     # cluster - the whole cluster has unified storage | ||||
|     type: node | ||||
|     # topology reported by CSI driver is reflected in k8s as node labels. | ||||
|     # you may want to set unique prefixes on different drivers to avoid collisions | ||||
|     prefix: org.democratic-csi.topology | ||||
| ``` | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue