Add cluster backup + restore functionality
Adds cluster backup (JSON dump) and restore functions for use in disaster recovery. Further, adds additional confirmation to the initialization (as well as restore) endpoints to avoid accidental triggering, and also groups the init, backup, and restore commands in the CLI into a new "task" subsection.
This commit is contained in:
@ -333,14 +333,23 @@ api.add_resource(API_Logout, '/logout')
|
||||
|
||||
# /initialize
|
||||
class API_Initialize(Resource):
|
||||
@RequestParser([
|
||||
{'name': 'yes-i-really-mean-it', 'required': True, 'helptext': "Initialization is destructive; please confirm with the argument 'yes-i-really-mean-it'."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self):
|
||||
def post(self, reqargs):
|
||||
"""
|
||||
Initialize a new PVC cluster
|
||||
Note: Normally used only once during cluster bootstrap; checks for the existence of the "/primary_node" key before proceeding and returns 400 if found
|
||||
---
|
||||
tags:
|
||||
- root
|
||||
parameters:
|
||||
- in: query
|
||||
name: yes-i-really-mean-it
|
||||
type: string
|
||||
required: true
|
||||
description: A confirmation string to ensure that the API consumer really means it
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
@ -363,6 +372,82 @@ class API_Initialize(Resource):
|
||||
api.add_resource(API_Initialize, '/initialize')
|
||||
|
||||
|
||||
# /backup
|
||||
class API_Backup(Resource):
|
||||
@Authenticator
|
||||
def get(self):
|
||||
"""
|
||||
Back up the Zookeeper data of a cluster in JSON format
|
||||
---
|
||||
tags:
|
||||
- root
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
schema:
|
||||
type: object
|
||||
id: Cluster Data
|
||||
400:
|
||||
description: Bad request
|
||||
"""
|
||||
return api_helper.backup_cluster()
|
||||
|
||||
|
||||
api.add_resource(API_Backup, '/backup')
|
||||
|
||||
|
||||
# /restore
|
||||
class API_Restore(Resource):
|
||||
@RequestParser([
|
||||
{'name': 'yes-i-really-mean-it', 'required': True, 'helptext': "Restore is destructive; please confirm with the argument 'yes-i-really-mean-it'."},
|
||||
{'name': 'cluster_data', 'required': True, 'helptext': "A cluster JSON backup must be provided."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, reqargs):
|
||||
"""
|
||||
Restore a backup over the cluster; destroys the existing data
|
||||
---
|
||||
tags:
|
||||
- root
|
||||
parameters:
|
||||
- in: query
|
||||
name: yes-i-really-mean-it
|
||||
type: string
|
||||
required: true
|
||||
description: A confirmation string to ensure that the API consumer really means it
|
||||
- in: query
|
||||
name: cluster_data
|
||||
type: string
|
||||
required: true
|
||||
description: The raw JSON cluster backup data
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
schema:
|
||||
type: object
|
||||
id: Message
|
||||
400:
|
||||
description: Bad request
|
||||
schema:
|
||||
type: object
|
||||
id: Message
|
||||
500:
|
||||
description: Restore error or code failure
|
||||
schema:
|
||||
type: object
|
||||
id: Message
|
||||
"""
|
||||
try:
|
||||
cluster_data = reqargs.get('cluster_data')
|
||||
except Exception as e:
|
||||
return {"message": "Failed to load JSON backup: {}.".format(e)}, 400
|
||||
|
||||
return api_helper.restore_cluster(cluster_data)
|
||||
|
||||
|
||||
api.add_resource(API_Restore, '/restore')
|
||||
|
||||
|
||||
# /status
|
||||
class API_Status(Resource):
|
||||
@Authenticator
|
||||
|
@ -21,6 +21,7 @@
|
||||
###############################################################################
|
||||
|
||||
import flask
|
||||
import json
|
||||
import lxml.etree as etree
|
||||
|
||||
from distutils.util import strtobool as dustrtobool
|
||||
@ -49,7 +50,7 @@ def strtobool(stringv):
|
||||
|
||||
|
||||
#
|
||||
# Initialization function
|
||||
# Cluster base functions
|
||||
#
|
||||
def initialize_cluster():
|
||||
# Open a Zookeeper connection
|
||||
@ -86,6 +87,66 @@ def initialize_cluster():
|
||||
return True
|
||||
|
||||
|
||||
def backup_cluster():
|
||||
# Open a zookeeper connection
|
||||
zk_conn = pvc_common.startZKConnection(config['coordinators'])
|
||||
|
||||
# Dictionary of values to come
|
||||
cluster_data = dict()
|
||||
|
||||
def get_data(path):
|
||||
data_raw = zk_conn.get(path)
|
||||
if data_raw:
|
||||
data = data_raw[0].decode('utf8')
|
||||
children = zk_conn.get_children(path)
|
||||
|
||||
cluster_data[path] = data
|
||||
|
||||
if children:
|
||||
if path == '/':
|
||||
child_prefix = '/'
|
||||
else:
|
||||
child_prefix = path + '/'
|
||||
|
||||
for child in children:
|
||||
if child_prefix + child == '/zookeeper':
|
||||
# We must skip the built-in /zookeeper tree
|
||||
continue
|
||||
get_data(child_prefix + child)
|
||||
|
||||
get_data('/')
|
||||
|
||||
return cluster_data, 200
|
||||
|
||||
|
||||
def restore_cluster(cluster_data_raw):
|
||||
# Open a zookeeper connection
|
||||
zk_conn = pvc_common.startZKConnection(config['coordinators'])
|
||||
|
||||
# Open a single transaction (restore is atomic)
|
||||
zk_transaction = zk_conn.transaction()
|
||||
|
||||
try:
|
||||
cluster_data = json.loads(cluster_data_raw)
|
||||
except Exception as e:
|
||||
return {"message": "Failed to parse JSON data: {}.".format(e)}, 400
|
||||
|
||||
for key in cluster_data:
|
||||
data = cluster_data[key]
|
||||
|
||||
if zk_conn.exists(key):
|
||||
zk_transaction.set_data(key, str(data).encode('utf8'))
|
||||
else:
|
||||
zk_transaction.create(key, str(data).encode('utf8'))
|
||||
|
||||
try:
|
||||
zk_transaction.commit()
|
||||
return {'message': 'Restore completed successfully.'}, 200
|
||||
except Exception as e:
|
||||
raise
|
||||
return {'message': 'Restore failed: {}.'.format(e)}, 500
|
||||
|
||||
|
||||
#
|
||||
# Cluster functions
|
||||
#
|
||||
|
Reference in New Issue
Block a user