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 { CsiBaseDriver } = require("../index"); | ||||||
| const { GrpcError, grpc } = require("../../utils/grpc"); | const { GrpcError, grpc } = require("../../utils/grpc"); | ||||||
| const cp = require("child_process"); | const cp = require("child_process"); | ||||||
| const { Mount } = require("../../utils/mount"); |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Crude nfs-client driver which simply creates directories to be mounted |  * 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 { ControllerZfsGenericDriver } = require("./controller-zfs-generic"); | ||||||
| const { | const { | ||||||
|   ZfsLocalEphemeralInlineDriver, |   ZfsLocalEphemeralInlineDriver, | ||||||
|  | @ -6,6 +7,7 @@ const { | ||||||
| 
 | 
 | ||||||
| const { ControllerNfsClientDriver } = require("./controller-nfs-client"); | const { ControllerNfsClientDriver } = require("./controller-nfs-client"); | ||||||
| const { ControllerSmbClientDriver } = require("./controller-smb-client"); | const { ControllerSmbClientDriver } = require("./controller-smb-client"); | ||||||
|  | const { ControllerSynologyDriver } = require("./controller-synology"); | ||||||
| const { NodeManualDriver } = require("./node-manual"); | const { NodeManualDriver } = require("./node-manual"); | ||||||
| 
 | 
 | ||||||
| function factory(ctx, options) { | function factory(ctx, options) { | ||||||
|  | @ -16,7 +18,15 @@ function factory(ctx, options) { | ||||||
|     case "truenas-nfs": |     case "truenas-nfs": | ||||||
|     case "truenas-smb": |     case "truenas-smb": | ||||||
|     case "truenas-iscsi": |     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-nfs": | ||||||
|     case "zfs-generic-iscsi": |     case "zfs-generic-iscsi": | ||||||
|       return new ControllerZfsGenericDriver(ctx, options); |       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