From 49e5ce11763146dde1551f24926f6f22375b1e87 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 9 Feb 2020 13:43:48 -0500 Subject: [PATCH] Support uploading disk images to volumes in API Addresses #68 --- api-daemon/pvcapid/flaskapi.py | 47 ++++++++++++++++++++++++++++++++++ api-daemon/pvcapid/helper.py | 43 +++++++++++++++++++++++++++++++ daemon-common/ceph.py | 38 ++++++++++++++++++++++++++- docs/manuals/swagger.json | 38 +++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 1 deletion(-) diff --git a/api-daemon/pvcapid/flaskapi.py b/api-daemon/pvcapid/flaskapi.py index 61ed1e4d..cc1820ab 100755 --- a/api-daemon/pvcapid/flaskapi.py +++ b/api-daemon/pvcapid/flaskapi.py @@ -3334,6 +3334,53 @@ class API_Storage_Ceph_Volume_Element_Clone(Resource): ) api.add_resource(API_Storage_Ceph_Volume_Element_Clone, '/storage/ceph/volume///clone') +# /storage/ceph/volume///upload +class API_Storage_Ceph_Volume_Element_Upload(Resource): + @Authenticator + def post(self, pool, volume): + """ + Upload a disk image to Ceph volume {volume} in pool {pool} + --- + tags: + - storage / ceph + parameters: + - in: query + name: file + type: binary + required: true + description: The raw binary contents of the file + responses: + 200: + description: OK + schema: + type: object + id: Message + 404: + description: Not found + schema: + type: object + id: Message + 400: + description: Bad request + schema: + type: object + id: Message + """ + from flask_restful import reqparse + from werkzeug.datastructures import FileStorage + parser = reqparse.RequestParser() + parser.add_argument('file', type=FileStorage, location='files') + data = parser.parse_args() + image_data = data.get('file', None) + if not image_data: + return {'message':'An image file contents must be specified'}, 400 + return api_helper.ceph_volume_upload( + pool, + volume, + image_data + ) +api.add_resource(API_Storage_Ceph_Volume_Element_Upload, '/storage/ceph/volume///upload') + # /storage/ceph/snapshot class API_Storage_Ceph_Snapshot_Root(Resource): @RequestParser([ diff --git a/api-daemon/pvcapid/helper.py b/api-daemon/pvcapid/helper.py index 11ae14e4..05107793 100755 --- a/api-daemon/pvcapid/helper.py +++ b/api-daemon/pvcapid/helper.py @@ -1327,6 +1327,49 @@ def ceph_volume_remove(pool, name): } return output, retcode +def ceph_volume_upload(pool, volume, data): + """ + Upload a raw file via HTTP post to a PVC Ceph volume + """ + # Map the target blockdev + zk_conn = pvc_common.startZKConnection(config['coordinators']) + retflag, retdata = pvc_ceph.map_volume(zk_conn, pool, volume) + pvc_common.stopZKConnection(zk_conn) + if not retflag: + output = { + 'message': retdata.replace('\"', '\'') + } + retcode = 400 + return output, retcode + blockdev = retdata + + output = { + 'message': "Wrote uploaded file to volume '{}' in pool '{}'.".format(volume, pool) + } + retcode = 200 + + # Save the data to the blockdev + try: + data.save(blockdev) + except: + output = { + 'message': "ERROR: Failed to write image file to volume." + } + retcode = 400 + + # Unmap the target blockdev + zk_conn = pvc_common.startZKConnection(config['coordinators']) + retflag, retdata = pvc_ceph.unmap_volume(zk_conn, pool, volume) + pvc_common.stopZKConnection(zk_conn) + if not retflag: + output = { + 'message': retdata.replace('\"', '\'') + } + retcode = 400 + return output, retcode + + return output, retcode + def ceph_volume_snapshot_list(pool=None, volume=None, limit=None, is_fuzzy=True): """ Get the list of RBD volume snapshots in the Ceph storage cluster. diff --git a/daemon-common/ceph.py b/daemon-common/ceph.py index 418607ae..9380e1e5 100644 --- a/daemon-common/ceph.py +++ b/daemon-common/ceph.py @@ -20,6 +20,7 @@ # ############################################################################### +import os import re import click import json @@ -903,7 +904,7 @@ def add_volume(zk_conn, pool, name, size): '/ceph/snapshots/{}/{}'.format(pool, name): '', }) - return True, 'Created RBD volume "{}/{}" ({})'.format(pool, name, size) + return True, 'Created RBD volume "{}/{}" ({}).'.format(pool, name, size) def clone_volume(zk_conn, pool, name_src, name_new): if not verifyVolume(zk_conn, pool, name_src): @@ -994,6 +995,41 @@ def remove_volume(zk_conn, pool, name): return True, 'Removed RBD volume "{}" in pool "{}".'.format(name, pool) +def map_volume(zk_conn, pool, name): + if not verifyVolume(zk_conn, pool, name): + return False, 'ERROR: No volume with name "{}" is present in pool "{}".'.format(name, pool) + + # 1. Map the volume onto the local system + retcode, stdout, stderr = common.run_os_command('rbd map {}/{}'.format(pool, name)) + if retcode: + return False, 'ERROR: Failed to map RBD volume "{}" in pool "{}": {}'.format(name, pool, stderr) + + # 2. Calculate the absolute path to the mapped volume + mapped_volume = '/dev/rbd/{}/{}'.format(pool, name) + + # 3. Ensure the volume exists + if not os.path.exists(mapped_volume): + return False, 'ERROR: Mapped volume not found at expected location "{}".'.format(mapped_volume) + + return True, mapped_volume + +def unmap_volume(zk_conn, pool, name): + if not verifyVolume(zk_conn, pool, name): + return False, 'ERROR: No volume with name "{}" is present in pool "{}".'.format(name, pool) + + mapped_volume = '/dev/rbd/{}/{}'.format(pool, name) + + # 1. Ensure the volume exists + if not os.path.exists(mapped_volume): + return False, 'ERROR: Mapped volume not found at expected location "{}".'.format(mapped_volume) + + # 2. Unap the volume + retcode, stdout, stderr = common.run_os_command('rbd unmap {}'.format(mapped_volume)) + if retcode: + return False, 'ERROR: Failed to unmap RBD volume at "{}": {}'.format(mapped_volume, stderr) + + return True, 'Unmapped RBD volume at "{}".'.format(mapped_volume) + def get_list_volume(zk_conn, pool, limit, is_fuzzy=True): volume_list = [] if pool and not verifyPool(zk_conn, pool): diff --git a/docs/manuals/swagger.json b/docs/manuals/swagger.json index 54c6dede..d79709d0 100644 --- a/docs/manuals/swagger.json +++ b/docs/manuals/swagger.json @@ -4691,6 +4691,44 @@ ] } }, + "/api/v1/storage/ceph/volume/{pool}/{volume}/upload": { + "post": { + "description": "", + "parameters": [ + { + "description": "The raw binary contents of the file", + "in": "query", + "name": "file", + "required": true, + "type": "binary" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Message" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/Message" + } + }, + "404": { + "description": "Not found", + "schema": { + "$ref": "#/definitions/Message" + } + } + }, + "summary": "Upload a disk image to Ceph volume {volume} in pool {pool}", + "tags": [ + "storage / ceph" + ] + } + }, "/api/v1/vm": { "get": { "description": "",