freenas overhaul, synology shell
Signed-off-by: Travis Glenn Hansen <travisghansen@yahoo.com>
This commit is contained in:
		
							parent
							
								
									8765da65c4
								
							
						
					
					
						commit
						8b6e12dd77
					
				|  | @ -1,7 +1,6 @@ | |||
| const { CsiBaseDriver } = require("../index"); | ||||
| const { GrpcError, grpc } = require("../../utils/grpc"); | ||||
| const cp = require("child_process"); | ||||
| const { Mount } = require("../../utils/mount"); | ||||
| 
 | ||||
| /** | ||||
|  * Crude nfs-client driver which simply creates directories to be mounted | ||||
|  |  | |||
|  | @ -0,0 +1,465 @@ | |||
| const { CsiBaseDriver } = require("../index"); | ||||
| const { GrpcError, grpc } = require("../../utils/grpc"); | ||||
| 
 | ||||
| /** | ||||
|  * | ||||
|  * Driver to provision storage on a synology device | ||||
|  * | ||||
|  */ | ||||
| class ControllerSynologyDriver extends CsiBaseDriver { | ||||
|   constructor(ctx, options) { | ||||
|     super(...arguments); | ||||
| 
 | ||||
|     options = options || {}; | ||||
|     options.service = options.service || {}; | ||||
|     options.service.identity = options.service.identity || {}; | ||||
|     options.service.controller = options.service.controller || {}; | ||||
|     options.service.node = options.service.node || {}; | ||||
| 
 | ||||
|     options.service.identity.capabilities = | ||||
|       options.service.identity.capabilities || {}; | ||||
| 
 | ||||
|     options.service.controller.capabilities = | ||||
|       options.service.controller.capabilities || {}; | ||||
| 
 | ||||
|     options.service.node.capabilities = options.service.node.capabilities || {}; | ||||
| 
 | ||||
|     if (!("service" in options.service.identity.capabilities)) { | ||||
|       this.ctx.logger.debug("setting default identity service caps"); | ||||
| 
 | ||||
|       options.service.identity.capabilities.service = [ | ||||
|         //"UNKNOWN",
 | ||||
|         "CONTROLLER_SERVICE", | ||||
|         //"VOLUME_ACCESSIBILITY_CONSTRAINTS"
 | ||||
|       ]; | ||||
|     } | ||||
| 
 | ||||
|     if (!("volume_expansion" in options.service.identity.capabilities)) { | ||||
|       this.ctx.logger.debug("setting default identity volume_expansion caps"); | ||||
| 
 | ||||
|       options.service.identity.capabilities.volume_expansion = [ | ||||
|         //"UNKNOWN",
 | ||||
|         "ONLINE", | ||||
|         //"OFFLINE"
 | ||||
|       ]; | ||||
|     } | ||||
| 
 | ||||
|     if (!("rpc" in options.service.controller.capabilities)) { | ||||
|       this.ctx.logger.debug("setting default controller caps"); | ||||
| 
 | ||||
|       options.service.controller.capabilities.rpc = [ | ||||
|         //"UNKNOWN",
 | ||||
|         "CREATE_DELETE_VOLUME", | ||||
|         //"PUBLISH_UNPUBLISH_VOLUME",
 | ||||
|         //"LIST_VOLUMES",
 | ||||
|         //"GET_CAPACITY",
 | ||||
|         //"CREATE_DELETE_SNAPSHOT",
 | ||||
|         //"LIST_SNAPSHOTS",
 | ||||
|         //"CLONE_VOLUME",
 | ||||
|         //"PUBLISH_READONLY",
 | ||||
|         //"EXPAND_VOLUME",
 | ||||
|       ]; | ||||
|     } | ||||
| 
 | ||||
|     if (!("rpc" in options.service.node.capabilities)) { | ||||
|       this.ctx.logger.debug("setting default node caps"); | ||||
| 
 | ||||
|       options.service.node.capabilities.rpc = [ | ||||
|         //"UNKNOWN",
 | ||||
|         "STAGE_UNSTAGE_VOLUME", | ||||
|         "GET_VOLUME_STATS", | ||||
|         //"EXPAND_VOLUME"
 | ||||
|       ]; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   getDriverResourceType() { | ||||
|     switch (this.options.driver) { | ||||
|       case "synology-nfs": | ||||
|       case "synology-smb": | ||||
|         return "filesystem"; | ||||
|       case "synology-iscsi": | ||||
|         return "volume"; | ||||
|       default: | ||||
|         throw new Error("unknown driver: " + this.ctx.args.driver); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   getDriverShareType() { | ||||
|     switch (this.options.driver) { | ||||
|       case "synology-nfs": | ||||
|         return "nfs"; | ||||
|       case "synology-smb": | ||||
|         return "smb"; | ||||
|       case "synology-iscsi": | ||||
|         return "iscsi"; | ||||
|       default: | ||||
|         throw new Error("unknown driver: " + this.ctx.args.driver); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   assertCapabilities(capabilities) { | ||||
|     const driverResourceType = this.getDriverResourceType(); | ||||
|     this.ctx.logger.verbose("validating capabilities: %j", capabilities); | ||||
| 
 | ||||
|     let message = null; | ||||
|     //[{"access_mode":{"mode":"SINGLE_NODE_WRITER"},"mount":{"mount_flags":["noatime","_netdev"],"fs_type":"nfs"},"access_type":"mount"}]
 | ||||
|     const valid = capabilities.every((capability) => { | ||||
|       switch (driverResourceType) { | ||||
|         case "filesystem": | ||||
|           if (capability.access_type != "mount") { | ||||
|             message = `invalid access_type ${capability.access_type}`; | ||||
|             return false; | ||||
|           } | ||||
| 
 | ||||
|           if ( | ||||
|             capability.mount.fs_type && | ||||
|             !["nfs", "cifs"].includes(capability.mount.fs_type) | ||||
|           ) { | ||||
|             message = `invalid fs_type ${capability.mount.fs_type}`; | ||||
|             return false; | ||||
|           } | ||||
| 
 | ||||
|           if ( | ||||
|             ![ | ||||
|               "UNKNOWN", | ||||
|               "SINGLE_NODE_WRITER", | ||||
|               "SINGLE_NODE_READER_ONLY", | ||||
|               "MULTI_NODE_READER_ONLY", | ||||
|               "MULTI_NODE_SINGLE_WRITER", | ||||
|               "MULTI_NODE_MULTI_WRITER", | ||||
|             ].includes(capability.access_mode.mode) | ||||
|           ) { | ||||
|             message = `invalid access_mode, ${capability.access_mode.mode}`; | ||||
|             return false; | ||||
|           } | ||||
| 
 | ||||
|           return true; | ||||
|         case "volume": | ||||
|           if (capability.access_type == "mount") { | ||||
|             if ( | ||||
|               capability.mount.fs_type && | ||||
|               !["ext3", "ext4", "ext4dev", "xfs"].includes( | ||||
|                 capability.mount.fs_type | ||||
|               ) | ||||
|             ) { | ||||
|               message = `invalid fs_type ${capability.mount.fs_type}`; | ||||
|               return false; | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           if ( | ||||
|             ![ | ||||
|               "UNKNOWN", | ||||
|               "SINGLE_NODE_WRITER", | ||||
|               "SINGLE_NODE_READER_ONLY", | ||||
|               "MULTI_NODE_READER_ONLY", | ||||
|               "MULTI_NODE_SINGLE_WRITER", | ||||
|             ].includes(capability.access_mode.mode) | ||||
|           ) { | ||||
|             message = `invalid access_mode, ${capability.access_mode.mode}`; | ||||
|             return false; | ||||
|           } | ||||
| 
 | ||||
|           return true; | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     return { valid, message }; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * CreateVolume | ||||
|    * | ||||
|    * @param {*} call | ||||
|    */ | ||||
|   async CreateVolume(call) { | ||||
|     const driver = this; | ||||
| 
 | ||||
|     let name = call.request.name; | ||||
|     let volume_content_source = call.request.volume_content_source; | ||||
| 
 | ||||
|     if (!name) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         `volume name is required` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (call.request.volume_capabilities) { | ||||
|       const result = this.assertCapabilities(call.request.volume_capabilities); | ||||
|       if (result.valid !== true) { | ||||
|         throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       call.request.capacity_range.required_bytes > 0 && | ||||
|       call.request.capacity_range.limit_bytes > 0 && | ||||
|       call.request.capacity_range.required_bytes > | ||||
|         call.request.capacity_range.limit_bytes | ||||
|     ) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.OUT_OF_RANGE, | ||||
|         `required_bytes is greather than limit_bytes` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     let capacity_bytes = | ||||
|       call.request.capacity_range.required_bytes || | ||||
|       call.request.capacity_range.limit_bytes; | ||||
| 
 | ||||
|     if (!capacity_bytes) { | ||||
|       //should never happen, value must be set
 | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         `volume capacity is required (either required_bytes or limit_bytes)` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     // ensure *actual* capacity is not greater than limit
 | ||||
|     if ( | ||||
|       call.request.capacity_range.limit_bytes && | ||||
|       call.request.capacity_range.limit_bytes > 0 && | ||||
|       capacity_bytes > call.request.capacity_range.limit_bytes | ||||
|     ) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.OUT_OF_RANGE, | ||||
|         `required volume capacity is greater than limit` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     switch (driver.getDriverShareType()) { | ||||
|       case "nfs": | ||||
|         // TODO: create volume here
 | ||||
|         break; | ||||
|       case "smb": | ||||
|         // TODO: create volume here
 | ||||
|         break; | ||||
|       case "iscsi": | ||||
|         // TODO: create volume here
 | ||||
|         break; | ||||
|       default: | ||||
|         // throw an error
 | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     let volume_context = driver.getVolumeContext(name); | ||||
| 
 | ||||
|     volume_context["provisioner_driver"] = driver.options.driver; | ||||
|     if (driver.options.instance_id) { | ||||
|       volume_context["provisioner_driver_instance_id"] = | ||||
|         driver.options.instance_id; | ||||
|     } | ||||
| 
 | ||||
|     const res = { | ||||
|       volume: { | ||||
|         volume_id: name, | ||||
|         //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0
 | ||||
|         capacity_bytes: 0, | ||||
|         content_source: volume_content_source, | ||||
|         volume_context, | ||||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|     return res; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * DeleteVolume | ||||
|    * | ||||
|    * @param {*} call | ||||
|    */ | ||||
|   async DeleteVolume(call) { | ||||
|     const driver = this; | ||||
| 
 | ||||
|     let name = call.request.volume_id; | ||||
| 
 | ||||
|     if (!name) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         `volume_id is required` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     switch (driver.getDriverShareType()) { | ||||
|       case "nfs": | ||||
|         // TODO: delete volume here
 | ||||
|         break; | ||||
|       case "smb": | ||||
|         // TODO: delete volume here
 | ||||
|         break; | ||||
|       case "iscsi": | ||||
|         // TODO: delete volume here
 | ||||
|         break; | ||||
|       default: | ||||
|         // throw an error
 | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     return {}; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {*} call | ||||
|    */ | ||||
|   async ControllerExpandVolume(call) { | ||||
|     throw new GrpcError( | ||||
|       grpc.status.UNIMPLEMENTED, | ||||
|       `operation not supported by driver` | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * TODO: consider volume_capabilities? | ||||
|    * | ||||
|    * @param {*} call | ||||
|    */ | ||||
|   async GetCapacity(call) { | ||||
|     throw new GrpcError( | ||||
|       grpc.status.UNIMPLEMENTED, | ||||
|       `operation not supported by driver` | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * TODO: check capability to ensure not asking about block volumes | ||||
|    * | ||||
|    * @param {*} call | ||||
|    */ | ||||
|   async ListVolumes(call) { | ||||
|     throw new GrpcError( | ||||
|       grpc.status.UNIMPLEMENTED, | ||||
|       `operation not supported by driver` | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {*} call | ||||
|    */ | ||||
|   async ListSnapshots(call) { | ||||
|     throw new GrpcError( | ||||
|       grpc.status.UNIMPLEMENTED, | ||||
|       `operation not supported by driver` | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {*} call | ||||
|    */ | ||||
|   async CreateSnapshot(call) { | ||||
|     throw new GrpcError( | ||||
|       grpc.status.UNIMPLEMENTED, | ||||
|       `operation not supported by driver` | ||||
|     ); | ||||
|     const driver = this; | ||||
| 
 | ||||
|     // both these are required
 | ||||
|     let source_volume_id = call.request.source_volume_id; | ||||
|     let name = call.request.name; | ||||
| 
 | ||||
|     if (!source_volume_id) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         `snapshot source_volume_id is required` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (!name) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         `snapshot name is required` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     driver.ctx.logger.verbose("requested snapshot name: %s", name); | ||||
| 
 | ||||
|     let invalid_chars; | ||||
|     invalid_chars = name.match(/[^a-z0-9_\-:.+]+/gi); | ||||
|     if (invalid_chars) { | ||||
|       invalid_chars = String.prototype.concat( | ||||
|         ...new Set(invalid_chars.join("")) | ||||
|       ); | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         `snapshot name contains invalid characters: ${invalid_chars}` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     // TODO: create snapshot here
 | ||||
| 
 | ||||
|     return { | ||||
|       snapshot: { | ||||
|         /** | ||||
|          * The purpose of this field is to give CO guidance on how much space | ||||
|          * is needed to create a volume from this snapshot. | ||||
|          */ | ||||
|         size_bytes: 0, | ||||
|         snapshot_id, | ||||
|         source_volume_id: source_volume_id, | ||||
|         //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto
 | ||||
|         creation_time: { | ||||
|           seconds: Math.round(new Date().getTime() / 1000), | ||||
|           nanos: 0, | ||||
|         }, | ||||
|         ready_to_use: true, | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * In addition, if clones have been created from a snapshot, then they must | ||||
|    * be destroyed before the snapshot can be destroyed. | ||||
|    * | ||||
|    * @param {*} call | ||||
|    */ | ||||
|   async DeleteSnapshot(call) { | ||||
|     throw new GrpcError( | ||||
|       grpc.status.UNIMPLEMENTED, | ||||
|       `operation not supported by driver` | ||||
|     ); | ||||
| 
 | ||||
|     const driver = this; | ||||
| 
 | ||||
|     const snapshot_id = call.request.snapshot_id; | ||||
| 
 | ||||
|     if (!snapshot_id) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         `snapshot_id is required` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     // TODO: delete snapshot here
 | ||||
| 
 | ||||
|     return {}; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {*} call | ||||
|    */ | ||||
|   async ValidateVolumeCapabilities(call) { | ||||
|     const driver = this; | ||||
|     const result = this.assertCapabilities(call.request.volume_capabilities); | ||||
| 
 | ||||
|     if (result.valid !== true) { | ||||
|       return { message: result.message }; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       confirmed: { | ||||
|         volume_context: call.request.volume_context, | ||||
|         volume_capabilities: call.request.volume_capabilities, // TODO: this is a bit crude, should return *ALL* capabilities, not just what was requested
 | ||||
|         parameters: call.request.parameters, | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| module.exports.ControllerSynologyDriver = ControllerSynologyDriver; | ||||
|  | @ -1,4 +1,5 @@ | |||
| const { FreeNASDriver } = require("./freenas"); | ||||
| const { FreeNASSshDriver } = require("./freenas/ssh"); | ||||
| const { FreeNASApiDriver } = require("./freenas/api"); | ||||
| const { ControllerZfsGenericDriver } = require("./controller-zfs-generic"); | ||||
| const { | ||||
|   ZfsLocalEphemeralInlineDriver, | ||||
|  | @ -6,6 +7,7 @@ const { | |||
| 
 | ||||
| const { ControllerNfsClientDriver } = require("./controller-nfs-client"); | ||||
| const { ControllerSmbClientDriver } = require("./controller-smb-client"); | ||||
| const { ControllerSynologyDriver } = require("./controller-synology"); | ||||
| const { NodeManualDriver } = require("./node-manual"); | ||||
| 
 | ||||
| function factory(ctx, options) { | ||||
|  | @ -16,7 +18,15 @@ function factory(ctx, options) { | |||
|     case "truenas-nfs": | ||||
|     case "truenas-smb": | ||||
|     case "truenas-iscsi": | ||||
|       return new FreeNASDriver(ctx, options); | ||||
|       return new FreeNASSshDriver(ctx, options); | ||||
|     case "freenas-api-iscsi": | ||||
|     case "freenas-api-nfs": | ||||
|     case "freenas-api-smb": | ||||
|       return new FreeNASApiDriver(ctx, options); | ||||
|     case "synology-nfs": | ||||
|     case "synology-smb": | ||||
|     case "synology-iscsi": | ||||
|       return new ControllerSynologyDriver(ctx, options); | ||||
|     case "zfs-generic-nfs": | ||||
|     case "zfs-generic-iscsi": | ||||
|       return new ControllerZfsGenericDriver(ctx, options); | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,705 @@ | |||
| const { Zetabyte } = require("../../../utils/zfs"); | ||||
| 
 | ||||
| // used for in-memory cache of the version info
 | ||||
| const FREENAS_SYSTEM_VERSION_CACHE_KEY = "freenas:system_version"; | ||||
| 
 | ||||
| class Api { | ||||
|   constructor(client, cache, options = {}) { | ||||
|     this.client = client; | ||||
|     this.cache = cache; | ||||
|     this.options = options; | ||||
|   } | ||||
| 
 | ||||
|   async getHttpClient() { | ||||
|     return this.client; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * only here for the helpers | ||||
|    * @returns | ||||
|    */ | ||||
|   async getZetabyte() { | ||||
|     return new Zetabyte({ | ||||
|       executor: { | ||||
|         spawn: function () { | ||||
|           throw new Error( | ||||
|             "cannot use the zb implementation to execute zfs commands, must use the http api" | ||||
|           ); | ||||
|         }, | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   async findResourceByProperties(endpoint, match) { | ||||
|     if (!match) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (typeof match === "object" && Object.keys(match).length < 1) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const httpClient = await this.getHttpClient(); | ||||
|     let target; | ||||
|     let page = 0; | ||||
|     let lastReponse; | ||||
| 
 | ||||
|     // loop and find target
 | ||||
|     let queryParams = {}; | ||||
|     queryParams.limit = 100; | ||||
|     queryParams.offset = 0; | ||||
| 
 | ||||
|     while (!target) { | ||||
|       //Content-Range: items 0-2/3 (full set)
 | ||||
|       //Content-Range: items 0--1/3 (invalid offset)
 | ||||
|       if (queryParams.hasOwnProperty("offset")) { | ||||
|         queryParams.offset = queryParams.limit * page; | ||||
|       } | ||||
| 
 | ||||
|       // crude stoppage attempt
 | ||||
|       let response = await httpClient.get(endpoint, queryParams); | ||||
|       if (lastReponse) { | ||||
|         if (JSON.stringify(lastReponse) == JSON.stringify(response)) { | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|       lastReponse = response; | ||||
| 
 | ||||
|       if (response.statusCode == 200) { | ||||
|         if (response.body.length < 1) { | ||||
|           break; | ||||
|         } | ||||
|         response.body.some((i) => { | ||||
|           let isMatch = true; | ||||
| 
 | ||||
|           if (typeof match === "function") { | ||||
|             isMatch = match(i); | ||||
|           } else { | ||||
|             for (let property in match) { | ||||
|               if (match[property] != i[property]) { | ||||
|                 isMatch = false; | ||||
|                 break; | ||||
|               } | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           if (isMatch) { | ||||
|             target = i; | ||||
|             return true; | ||||
|           } | ||||
| 
 | ||||
|           return false; | ||||
|         }); | ||||
|       } else { | ||||
|         throw new Error( | ||||
|           "FreeNAS http error - code: " + | ||||
|             response.statusCode + | ||||
|             " body: " + | ||||
|             JSON.stringify(response.body) | ||||
|         ); | ||||
|       } | ||||
|       page++; | ||||
|     } | ||||
| 
 | ||||
|     return target; | ||||
|   } | ||||
| 
 | ||||
|   async getApiVersion() { | ||||
|     const systemVersion = await this.getSystemVersion(); | ||||
| 
 | ||||
|     if (systemVersion.v2) { | ||||
|       if ((await this.getSystemVersionMajorMinor()) == 11.2) { | ||||
|         return 1; | ||||
|       } | ||||
|       return 2; | ||||
|     } | ||||
| 
 | ||||
|     return 1; | ||||
|   } | ||||
| 
 | ||||
|   async getIsFreeNAS() { | ||||
|     const systemVersion = await this.getSystemVersion(); | ||||
|     let version; | ||||
| 
 | ||||
|     if (systemVersion.v2) { | ||||
|       version = systemVersion.v2; | ||||
|     } else { | ||||
|       version = systemVersion.v1.fullversion; | ||||
|     } | ||||
| 
 | ||||
|     if (version.toLowerCase().includes("freenas")) { | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   async getIsTrueNAS() { | ||||
|     const systemVersion = await this.getSystemVersion(); | ||||
|     let version; | ||||
| 
 | ||||
|     if (systemVersion.v2) { | ||||
|       version = systemVersion.v2; | ||||
|     } else { | ||||
|       version = systemVersion.v1.fullversion; | ||||
|     } | ||||
| 
 | ||||
|     if (version.toLowerCase().includes("truenas")) { | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   async getIsScale() { | ||||
|     const systemVersion = await this.getSystemVersion(); | ||||
| 
 | ||||
|     if (systemVersion.v2 && systemVersion.v2.toLowerCase().includes("scale")) { | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   async getSystemVersionMajorMinor() { | ||||
|     const systemVersion = await this.getSystemVersion(); | ||||
|     let parts; | ||||
|     let parts_i; | ||||
|     let version; | ||||
| 
 | ||||
|     /* | ||||
|     systemVersion.v2 = "FreeNAS-11.2-U5"; | ||||
|     systemVersion.v2 = "TrueNAS-SCALE-20.11-MASTER-20201127-092915"; | ||||
|     systemVersion.v1 = { | ||||
|       fullversion: "FreeNAS-9.3-STABLE-201503200528", | ||||
|       fullversion: "FreeNAS-11.2-U5 (c129415c52)", | ||||
|     }; | ||||
| 
 | ||||
|     systemVersion.v2 = null; | ||||
|     */ | ||||
| 
 | ||||
|     if (systemVersion.v2) { | ||||
|       version = systemVersion.v2; | ||||
|     } else { | ||||
|       version = systemVersion.v1.fullversion; | ||||
|     } | ||||
| 
 | ||||
|     if (version) { | ||||
|       parts = version.split("-"); | ||||
|       parts_i = []; | ||||
|       parts.forEach((value) => { | ||||
|         let i = value.replace(/[^\d.]/g, ""); | ||||
|         if (i.length > 0) { | ||||
|           parts_i.push(i); | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       // join and resplit to deal with single elements which contain a decimal
 | ||||
|       parts_i = parts_i.join(".").split("."); | ||||
|       parts_i.splice(2); | ||||
|       return parts_i.join("."); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async getSystemVersionMajor() { | ||||
|     const majorMinor = await this.getSystemVersionMajorMinor(); | ||||
|     return majorMinor.split(".")[0]; | ||||
|   } | ||||
| 
 | ||||
|   async setVersionInfoCache(versionInfo) { | ||||
|     await this.cache.set( | ||||
|       FREENAS_SYSTEM_VERSION_CACHE_KEY, | ||||
|       versionInfo, | ||||
|       60 * 1000 | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   async getSystemVersion() { | ||||
|     let cacheData = await this.cache.get(FREENAS_SYSTEM_VERSION_CACHE_KEY); | ||||
| 
 | ||||
|     if (cacheData) { | ||||
|       return cacheData; | ||||
|     } | ||||
| 
 | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
|     const endpoint = "/system/version/"; | ||||
|     let response; | ||||
|     const startApiVersion = httpClient.getApiVersion(); | ||||
|     const versionInfo = {}; | ||||
|     const versionErrors = {}; | ||||
|     const versionResponses = {}; | ||||
| 
 | ||||
|     httpClient.setApiVersion(2); | ||||
|     /** | ||||
|      * FreeNAS-11.2-U5 | ||||
|      * TrueNAS-12.0-RELEASE | ||||
|      * TrueNAS-SCALE-20.11-MASTER-20201127-092915 | ||||
|      */ | ||||
|     try { | ||||
|       response = await httpClient.get(endpoint); | ||||
|       versionResponses.v2 = response; | ||||
|       if (response.statusCode == 200) { | ||||
|         versionInfo.v2 = response.body; | ||||
| 
 | ||||
|         // return immediately to save on resources and silly requests
 | ||||
|         await this.setVersionInfoCache(versionInfo); | ||||
| 
 | ||||
|         // reset apiVersion
 | ||||
|         httpClient.setApiVersion(startApiVersion); | ||||
| 
 | ||||
|         return versionInfo; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       // if more info is needed use e.stack
 | ||||
|       versionErrors.v2 = e.toString(); | ||||
|     } | ||||
| 
 | ||||
|     httpClient.setApiVersion(1); | ||||
|     /** | ||||
|      * {"fullversion": "FreeNAS-9.3-STABLE-201503200528", "name": "FreeNAS", "version": "9.3"} | ||||
|      * {"fullversion": "FreeNAS-11.2-U5 (c129415c52)", "name": "FreeNAS", "version": ""} | ||||
|      */ | ||||
|     try { | ||||
|       response = await httpClient.get(endpoint); | ||||
|       versionResponses.v1 = response; | ||||
|       if (response.statusCode == 200 && IsJsonString(response.body)) { | ||||
|         versionInfo.v1 = response.body; | ||||
|         await this.setVersionInfoCache(versionInfo); | ||||
| 
 | ||||
|         // reset apiVersion
 | ||||
|         httpClient.setApiVersion(startApiVersion); | ||||
| 
 | ||||
|         return versionInfo; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       // if more info is needed use e.stack
 | ||||
|       versionErrors.v1 = e.toString(); | ||||
|     } | ||||
| 
 | ||||
|     // throw error if cannot get v1 or v2 data
 | ||||
|     // likely bad creds/url
 | ||||
|     throw new GrpcError( | ||||
|       grpc.status.UNKNOWN, | ||||
|       `FreeNAS error getting system version info: ${JSON.stringify({ | ||||
|         errors: versionErrors, | ||||
|         responses: versionResponses, | ||||
|       })}` | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   getIsUserProperty(property) { | ||||
|     if (property.includes(":")) { | ||||
|       return true; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   getUserProperties(properties) { | ||||
|     let user_properties = {}; | ||||
|     for (const property in properties) { | ||||
|       if (this.getIsUserProperty(property)) { | ||||
|         user_properties[property] = properties[property]; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return user_properties; | ||||
|   } | ||||
| 
 | ||||
|   getSystemProperties(properties) { | ||||
|     let system_properties = {}; | ||||
|     for (const property in properties) { | ||||
|       if (!this.getIsUserProperty(property)) { | ||||
|         system_properties[property] = properties[property]; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return system_properties; | ||||
|   } | ||||
| 
 | ||||
|   getPropertiesKeyValueArray(properties) { | ||||
|     let arr = []; | ||||
|     for (const property in properties) { | ||||
|       arr.push({ key: property, value: properties[property] }); | ||||
|     } | ||||
| 
 | ||||
|     return arr; | ||||
|   } | ||||
| 
 | ||||
|   async DatasetCreate(datasetName, data) { | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
|     let response; | ||||
|     let endpoint; | ||||
| 
 | ||||
|     data.name = datasetName; | ||||
| 
 | ||||
|     endpoint = "/pool/dataset"; | ||||
|     response = await httpClient.post(endpoint, data); | ||||
| 
 | ||||
|     if (response.statusCode == 200) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       response.statusCode == 422 && | ||||
|       JSON.stringify(response.body).includes("already exists") | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     throw new Error(JSON.stringify(response.body)); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {*} datasetName | ||||
|    * @param {*} data | ||||
|    * @returns | ||||
|    */ | ||||
|   async DatasetDelete(datasetName, data) { | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
|     let response; | ||||
|     let endpoint; | ||||
| 
 | ||||
|     endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; | ||||
|     response = await httpClient.delete(endpoint, data); | ||||
| 
 | ||||
|     if (response.statusCode == 200) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       response.statusCode == 422 && | ||||
|       JSON.stringify(response.body).includes("does not exist") | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     throw new Error(JSON.stringify(response.body)); | ||||
|   } | ||||
| 
 | ||||
|   async DatasetSet(datasetName, properties) { | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
|     let response; | ||||
|     let endpoint; | ||||
| 
 | ||||
|     endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; | ||||
|     response = await httpClient.put(endpoint, { | ||||
|       ...this.getSystemProperties(properties), | ||||
|       user_properties_update: this.getPropertiesKeyValueArray( | ||||
|         this.getUserProperties(properties) | ||||
|       ), | ||||
|     }); | ||||
| 
 | ||||
|     if (response.statusCode == 200) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     throw new Error(JSON.stringify(response.body)); | ||||
|   } | ||||
| 
 | ||||
|   async DatasetInherit(datasetName, property) { | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
|     let response; | ||||
|     let endpoint; | ||||
|     let system_properties = {}; | ||||
|     let user_properties_update = []; | ||||
| 
 | ||||
|     const isUserProperty = this.getIsUserProperty(property); | ||||
|     if (isUserProperty) { | ||||
|       user_properties_update = [{ key: property, remove: true }]; | ||||
|     } else { | ||||
|       system_properties[property] = "INHERIT"; | ||||
|     } | ||||
| 
 | ||||
|     endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; | ||||
|     response = await httpClient.put(endpoint, { | ||||
|       ...system_properties, | ||||
|       user_properties_update, | ||||
|     }); | ||||
| 
 | ||||
|     if (response.statusCode == 200) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     throw new Error(JSON.stringify(response.body)); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * zfs get -Hp all tank/k8s/test/PVC-111 | ||||
|    * | ||||
|    * @param {*} datasetName | ||||
|    * @param {*} properties | ||||
|    * @returns | ||||
|    */ | ||||
|   async DatasetGet(datasetName, properties) { | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
|     let response; | ||||
|     let endpoint; | ||||
| 
 | ||||
|     endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; | ||||
|     response = await httpClient.get(endpoint); | ||||
| 
 | ||||
|     if (response.statusCode == 200) { | ||||
|       let res = {}; | ||||
|       for (const property of properties) { | ||||
|         let p; | ||||
|         if (response.body.hasOwnProperty(property)) { | ||||
|           p = response.body[property]; | ||||
|         } else if (response.body.user_properties.hasOwnProperty(property)) { | ||||
|           p = response.body.user_properties[property]; | ||||
|         } else { | ||||
|           p = { | ||||
|             value: "-", | ||||
|             rawvalue: "-", | ||||
|             source: "-", | ||||
|           }; | ||||
|         } | ||||
| 
 | ||||
|         if (typeof p === "object" && p !== null) { | ||||
|           // nothing, leave as is
 | ||||
|         } else { | ||||
|           p = { | ||||
|             value: p, | ||||
|             rawvalue: p, | ||||
|           }; | ||||
|         } | ||||
| 
 | ||||
|         res[property] = p; | ||||
|       } | ||||
| 
 | ||||
|       return res; | ||||
|     } | ||||
| 
 | ||||
|     if (response.statusCode == 404) { | ||||
|       throw new Error("dataset does not exist"); | ||||
|     } | ||||
| 
 | ||||
|     throw new Error(JSON.stringify(response.body)); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * zfs get -Hp all tank/k8s/test/PVC-111 | ||||
|    * | ||||
|    * @param {*} snapshotName | ||||
|    * @param {*} properties | ||||
|    * @returns | ||||
|    */ | ||||
|   async SnapshotGet(snapshotName, properties) { | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
|     let response; | ||||
|     let endpoint; | ||||
| 
 | ||||
|     endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; | ||||
|     response = await httpClient.get(endpoint); | ||||
| 
 | ||||
|     if (response.statusCode == 200) { | ||||
|       let res = {}; | ||||
|       for (const property of properties) { | ||||
|         let p; | ||||
|         if (response.body.hasOwnProperty(property)) { | ||||
|           p = response.body[property]; | ||||
|         } else if (response.body.properties.hasOwnProperty(property)) { | ||||
|           p = response.body.properties[property]; | ||||
|         } else { | ||||
|           p = { | ||||
|             value: "-", | ||||
|             rawvalue: "-", | ||||
|             source: "-", | ||||
|           }; | ||||
|         } | ||||
| 
 | ||||
|         if (typeof p === "object" && p !== null) { | ||||
|           // nothing, leave as is
 | ||||
|         } else { | ||||
|           p = { | ||||
|             value: p, | ||||
|             rawvalue: p, | ||||
|           }; | ||||
|         } | ||||
| 
 | ||||
|         res[property] = p; | ||||
|       } | ||||
| 
 | ||||
|       return res; | ||||
|     } | ||||
| 
 | ||||
|     if (response.statusCode == 404) { | ||||
|       throw new Error("dataset does not exist"); | ||||
|     } | ||||
| 
 | ||||
|     throw new Error(JSON.stringify(response.body)); | ||||
|   } | ||||
| 
 | ||||
|   async SnapshotCreate(snapshotName, data = {}) { | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
|     const zb = await this.getZetabyte(); | ||||
| 
 | ||||
|     let response; | ||||
|     let endpoint; | ||||
| 
 | ||||
|     const dataset = zb.helpers.extractDatasetName(snapshotName); | ||||
|     const snapshot = zb.helpers.extractSnapshotName(snapshotName); | ||||
| 
 | ||||
|     data.dataset = dataset; | ||||
|     data.name = snapshot; | ||||
| 
 | ||||
|     endpoint = "/zfs/snapshot"; | ||||
|     response = await httpClient.post(endpoint, data); | ||||
| 
 | ||||
|     if (response.statusCode == 200) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       response.statusCode == 422 && | ||||
|       JSON.stringify(response.body).includes("already exists") | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     throw new Error(JSON.stringify(response.body)); | ||||
|   } | ||||
| 
 | ||||
|   async SnapshotDelete(snapshotName, data = {}) { | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
|     const zb = await this.getZetabyte(); | ||||
| 
 | ||||
|     let response; | ||||
|     let endpoint; | ||||
| 
 | ||||
|     endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; | ||||
|     response = await httpClient.delete(endpoint, data); | ||||
| 
 | ||||
|     if (response.statusCode == 200) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (response.statusCode == 404) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       response.statusCode == 422 && | ||||
|       JSON.stringify(response.body).includes("not found") | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     throw new Error(JSON.stringify(response.body)); | ||||
|   } | ||||
| 
 | ||||
|   async CloneCreate(snapshotName, datasetName, data = {}) { | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
|     const zb = await this.getZetabyte(); | ||||
| 
 | ||||
|     let response; | ||||
|     let endpoint; | ||||
| 
 | ||||
|     data.snapshot = snapshotName; | ||||
|     data.dataset_dst = datasetName; | ||||
| 
 | ||||
|     endpoint = "/zfs/snapshot/clone"; | ||||
|     response = await httpClient.post(endpoint, data); | ||||
| 
 | ||||
|     if (response.statusCode == 200) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       response.statusCode == 422 && | ||||
|       JSON.stringify(response.body).includes("already exists") | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     throw new Error(JSON.stringify(response.body)); | ||||
|   } | ||||
| 
 | ||||
|   // get all dataset snapshots
 | ||||
|   // https://github.com/truenas/middleware/pull/6934
 | ||||
|   // then use core.bulk to delete all
 | ||||
| 
 | ||||
|   async ReplicationRunOnetime(data) { | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
| 
 | ||||
|     let response; | ||||
|     let endpoint; | ||||
| 
 | ||||
|     endpoint = "/replication/run_onetime"; | ||||
|     response = await httpClient.post(endpoint, data); | ||||
| 
 | ||||
|     // 200 means the 'job' was accepted only
 | ||||
|     // must continue to check the status of the job to know when it has finished and if it was successful
 | ||||
|     // /core/get_jobs [["id", "=", jobidhere]]
 | ||||
|     if (response.statusCode == 200) { | ||||
|       return response.body; | ||||
|     } | ||||
| 
 | ||||
|     throw new Error(JSON.stringify(response.body)); | ||||
|   } | ||||
| 
 | ||||
|   async CoreGetJobs(data) { | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
| 
 | ||||
|     let response; | ||||
|     let endpoint; | ||||
| 
 | ||||
|     endpoint = "/core/get_jobs"; | ||||
|     response = await httpClient.get(endpoint, data); | ||||
| 
 | ||||
|     // 200 means the 'job' was accepted only
 | ||||
|     // must continue to check the status of the job to know when it has finished and if it was successful
 | ||||
|     // /core/get_jobs [["id", "=", jobidhere]]
 | ||||
|     if (response.statusCode == 200) { | ||||
|       return response.body; | ||||
|     } | ||||
| 
 | ||||
|     throw new Error(JSON.stringify(response.body)); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {*} data | ||||
|    */ | ||||
|   async FilesystemSetperm(data) { | ||||
|     /* | ||||
|       { | ||||
|         "path": "string", | ||||
|         "mode": "string", | ||||
|         "uid": 0, | ||||
|         "gid": 0, | ||||
|         "options": { | ||||
|           "stripacl": false, | ||||
|           "recursive": false, | ||||
|           "traverse": false | ||||
|         } | ||||
|       } | ||||
|     */ | ||||
| 
 | ||||
|     const httpClient = await this.getHttpClient(false); | ||||
|     let response; | ||||
|     let endpoint; | ||||
| 
 | ||||
|     endpoint = `/filesystem/setperm`; | ||||
|     response = await httpClient.post(endpoint, data); | ||||
| 
 | ||||
|     if (response.statusCode == 200) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     throw new Error(JSON.stringify(response.body)); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| function IsJsonString(str) { | ||||
|   try { | ||||
|     JSON.parse(str); | ||||
|   } catch (e) { | ||||
|     return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| module.exports.Api = Api; | ||||
		Loading…
	
		Reference in New Issue