From a95e72008eca4dd957870b3dc1cc767f991a7a08 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Fri, 2 Feb 2024 10:29:34 -0500 Subject: [PATCH] Add size validations for volume clones Adds the same validations as a volume add or resize to volume clones, to ensure there is enough free space for them. --- api-daemon/pvcapid/flaskapi.py | 14 ++++++++++++-- api-daemon/pvcapid/helper.py | 6 ++++-- client-cli/pvc/cli/cli.py | 16 ++++++++++++++-- client-cli/pvc/lib/storage.py | 6 +++--- daemon-common/ceph.py | 34 ++++++++++++++++++++++++++++++---- 5 files changed, 63 insertions(+), 13 deletions(-) diff --git a/api-daemon/pvcapid/flaskapi.py b/api-daemon/pvcapid/flaskapi.py index 87e26f85..422daf3d 100755 --- a/api-daemon/pvcapid/flaskapi.py +++ b/api-daemon/pvcapid/flaskapi.py @@ -5972,7 +5972,11 @@ class API_Storage_Ceph_Volume_Element_Clone(Resource): "name": "new_volume", "required": True, "helptext": "A new volume name must be specified.", - } + }, + { + "name": "force", + "required": False, + }, ] ) @Authenticator @@ -5988,6 +5992,12 @@ class API_Storage_Ceph_Volume_Element_Clone(Resource): type: string required: true description: The name of the new cloned volume + - in: query + name: force + type: boolean + required: false + default: flase + description: Force action if clone volume size would violate 80% full soft cap on the pool responses: 200: description: OK @@ -6006,7 +6016,7 @@ class API_Storage_Ceph_Volume_Element_Clone(Resource): id: Message """ return api_helper.ceph_volume_clone( - pool, reqargs.get("new_volume", None), volume + pool, reqargs.get("new_volume", None), volume, reqargs.get("force", None) ) diff --git a/api-daemon/pvcapid/helper.py b/api-daemon/pvcapid/helper.py index 6df257bd..97bd1829 100755 --- a/api-daemon/pvcapid/helper.py +++ b/api-daemon/pvcapid/helper.py @@ -1887,11 +1887,13 @@ def ceph_volume_add(zkhandler, pool, name, size, force_flag): @ZKConnection(config) -def ceph_volume_clone(zkhandler, pool, name, source_volume): +def ceph_volume_clone(zkhandler, pool, name, source_volume, force_flag): """ Clone a Ceph RBD volume to a new volume on the PVC Ceph storage cluster. """ - retflag, retdata = pvc_ceph.clone_volume(zkhandler, pool, source_volume, name) + retflag, retdata = pvc_ceph.clone_volume( + zkhandler, pool, source_volume, name, force_flag=force_flag + ) if retflag: retcode = 200 diff --git a/client-cli/pvc/cli/cli.py b/client-cli/pvc/cli/cli.py index c120e5e6..aea25275 100644 --- a/client-cli/pvc/cli/cli.py +++ b/client-cli/pvc/cli/cli.py @@ -4237,13 +4237,25 @@ def cli_storage_volume_rename(pool, name, new_name): @click.argument("pool") @click.argument("name") @click.argument("new_name") -def cli_storage_volume_clone(pool, name, new_name): +@click.option( + "-f", + "--force", + "force_flag", + is_flag=True, + default=False, + help="Force clone even if volume would violate 80% full safe free space.", +) +def cli_storage_volume_clone(pool, name, new_name, force_flag): """ Clone a Ceph RBD volume with name NAME in pool POOL to name NEW_NAME in pool POOL. + + PVC will prevent the clone of a volume who's new size is greater than the available free space on the pool. This cannot be overridden. + + PVC will prevent the clone of a volume who's new size is greater than the 80% full safe free space on the pool. This can be overridden with the "-f"/"--force" option but this may be dangerous! """ retcode, retmsg = pvc.lib.storage.ceph_volume_clone( - CLI_CONFIG, pool, name, new_name + CLI_CONFIG, pool, name, new_name, force_flag=force_flag ) finish(retcode, retmsg) diff --git a/client-cli/pvc/lib/storage.py b/client-cli/pvc/lib/storage.py index a46d1b9d..8b69c7c8 100644 --- a/client-cli/pvc/lib/storage.py +++ b/client-cli/pvc/lib/storage.py @@ -1294,15 +1294,15 @@ def ceph_volume_modify( return retstatus, response.json().get("message", "") -def ceph_volume_clone(config, pool, volume, new_volume): +def ceph_volume_clone(config, pool, volume, new_volume, force_flag=False): """ Clone Ceph volume API endpoint: POST /api/v1/storage/ceph/volume/{pool}/{volume} - API arguments: new_volume={new_volume + API arguments: new_volume={new_volume, force_flag={force_flag} API schema: {"message":"{data}"} """ - params = {"new_volume": new_volume} + params = {"new_volume": new_volume, "force_flag": force_flag} response = call_api( config, "post", diff --git a/daemon-common/ceph.py b/daemon-common/ceph.py index 388f7314..a12acd5c 100644 --- a/daemon-common/ceph.py +++ b/daemon-common/ceph.py @@ -611,13 +611,39 @@ def add_volume(zkhandler, pool, name, size, force_flag=False): ) -def clone_volume(zkhandler, pool, name_src, name_new): +def clone_volume(zkhandler, pool, name_src, name_new, force_flag=False): + # 1. Verify the volume if not verifyVolume(zkhandler, pool, name_src): return False, 'ERROR: No volume with name "{}" is present in pool "{}".'.format( name_src, pool ) - # 1. Clone the volume + volume_stats_raw = zkhandler.read(("volume.stats", f"{pool}/{name_src}")) + volume_stats = dict(json.loads(volume_stats_raw)) + size_bytes = volume_stats["size"] + pool_information = getPoolInformation(zkhandler, pool) + pool_total_free_bytes = int(pool_information["stats"]["free_bytes"]) + if size_bytes >= pool_total_free_bytes: + return ( + False, + f"ERROR: Clone volume size '{format_bytes_tohuman(size_bytes)}' is greater than the available free space in the pool ('{format_bytes_tohuman(pool_information['stats']['free_bytes'])}')", + ) + + # Check if we're greater than 80% utilization after the create; error if so unless we have the force flag + pool_total_bytes = ( + int(pool_information["stats"]["used_bytes"]) + pool_total_free_bytes + ) + pool_safe_total_bytes = int(pool_total_bytes * 0.80) + pool_safe_free_bytes = pool_safe_total_bytes - int( + pool_information["stats"]["used_bytes"] + ) + if size_bytes >= pool_safe_free_bytes and not force_flag: + return ( + False, + f"ERROR: Clone volume size '{format_bytes_tohuman(size_bytes)}' is greater than the safe free space in the pool ('{format_bytes_tohuman(pool_safe_free_bytes)}' for 80% full); retry with force to ignore this error", + ) + + # 2. Clone the volume retcode, stdout, stderr = common.run_os_command( "rbd copy {}/{} {}/{}".format(pool, name_src, pool, name_new) ) @@ -629,13 +655,13 @@ def clone_volume(zkhandler, pool, name_src, name_new): ), ) - # 2. Get volume stats + # 3. Get volume stats retcode, stdout, stderr = common.run_os_command( "rbd info --format json {}/{}".format(pool, name_new) ) volstats = stdout - # 3. Add the new volume to Zookeeper + # 4. Add the new volume to Zookeeper zkhandler.write( [ (("volume", f"{pool}/{name_new}"), ""),