Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
ce5ee11841 | |||
8f705c9cc2 | |||
3f2c7293d1 | |||
d4a28d7a58 | |||
e8914eabb7 | |||
e69eb93cb3 | |||
70dfcd434f | |||
0383f31086 |
14
README.md
14
README.md
@ -20,6 +20,20 @@ To get started with PVC, please see the [About](https://parallelvirtualcluster.r
|
||||
|
||||
## Changelog
|
||||
|
||||
#### v0.9.8
|
||||
|
||||
* Adds support for cluster backup/restore
|
||||
* Moves location of `init` command in CLI to make room for the above
|
||||
* Cleans up some invalid help messages from the API
|
||||
|
||||
#### v0.9.7
|
||||
|
||||
* Fixes bug with provisioner system template modifications
|
||||
|
||||
#### v0.9.6
|
||||
|
||||
* Fixes bug with migrations
|
||||
|
||||
#### v0.9.5
|
||||
|
||||
* Fixes bug with line count in log follow
|
||||
|
@ -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
|
||||
@ -443,7 +528,7 @@ class API_Status(Resource):
|
||||
return api_helper.cluster_status()
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'state', 'choices': ('true', 'false'), 'required': True, 'helpmsg': "A valid state must be specified."}
|
||||
{'name': 'state', 'choices': ('true', 'false'), 'required': True, 'helptext': "A valid state must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, reqargs):
|
||||
@ -2412,7 +2497,7 @@ api.add_resource(API_Network_Lease_Element, '/network/<vni>/lease/<mac>')
|
||||
class API_Network_ACL_Root(Resource):
|
||||
@RequestParser([
|
||||
{'name': 'limit'},
|
||||
{'name': 'direction', 'choices': ('in', 'out'), 'helpmsg': "A valid direction must be specified."}
|
||||
{'name': 'direction', 'choices': ('in', 'out'), 'helptext': "A valid direction must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def get(self, vni, reqargs):
|
||||
@ -2474,9 +2559,9 @@ class API_Network_ACL_Root(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'description', 'required': True, 'helpmsg': "A whitespace-free description must be specified."},
|
||||
{'name': 'rule', 'required': True, 'helpmsg': "A rule must be specified."},
|
||||
{'name': 'direction', 'choices': ('in', 'out'), 'helpmsg': "A valid direction must be specified."},
|
||||
{'name': 'description', 'required': True, 'helptext': "A whitespace-free description must be specified."},
|
||||
{'name': 'rule', 'required': True, 'helptext': "A rule must be specified."},
|
||||
{'name': 'direction', 'choices': ('in', 'out'), 'helptext': "A valid direction must be specified."},
|
||||
{'name': 'order'}
|
||||
])
|
||||
@Authenticator
|
||||
@ -2566,8 +2651,8 @@ class API_Network_ACL_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'rule', 'required': True, 'helpmsg': "A rule must be specified."},
|
||||
{'name': 'direction', 'choices': ('in', 'out'), 'helpmsg': "A valid direction must be specified."},
|
||||
{'name': 'rule', 'required': True, 'helptext': "A rule must be specified."},
|
||||
{'name': 'direction', 'choices': ('in', 'out'), 'helptext': "A valid direction must be specified."},
|
||||
{'name': 'order'}
|
||||
])
|
||||
@Authenticator
|
||||
@ -2858,7 +2943,7 @@ class API_Storage_Ceph_Benchmark(Resource):
|
||||
return api_benchmark.list_benchmarks(reqargs.get('job', None))
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'pool', 'required': True, 'helpmsg': "A valid pool must be specified."},
|
||||
{'name': 'pool', 'required': True, 'helptext': "A valid pool must be specified."},
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, reqargs):
|
||||
@ -2897,8 +2982,8 @@ api.add_resource(API_Storage_Ceph_Benchmark, '/storage/ceph/benchmark')
|
||||
# /storage/ceph/option
|
||||
class API_Storage_Ceph_Option(Resource):
|
||||
@RequestParser([
|
||||
{'name': 'option', 'required': True, 'helpmsg': "A valid option must be specified."},
|
||||
{'name': 'action', 'required': True, 'choices': ('set', 'unset'), 'helpmsg': "A valid action must be specified."},
|
||||
{'name': 'option', 'required': True, 'helptext': "A valid option must be specified."},
|
||||
{'name': 'action', 'required': True, 'choices': ('set', 'unset'), 'helptext': "A valid action must be specified."},
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, reqargs):
|
||||
@ -3039,9 +3124,9 @@ class API_Storage_Ceph_OSD_Root(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'node', 'required': True, 'helpmsg': "A valid node must be specified."},
|
||||
{'name': 'device', 'required': True, 'helpmsg': "A valid device must be specified."},
|
||||
{'name': 'weight', 'required': True, 'helpmsg': "An OSD weight must be specified."},
|
||||
{'name': 'node', 'required': True, 'helptext': "A valid node must be specified."},
|
||||
{'name': 'device', 'required': True, 'helptext': "A valid device must be specified."},
|
||||
{'name': 'weight', 'required': True, 'helptext': "An OSD weight must be specified."},
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, reqargs):
|
||||
@ -3109,7 +3194,7 @@ class API_Storage_Ceph_OSD_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'yes-i-really-mean-it', 'required': True, 'helpmsg': "Please confirm that 'yes-i-really-mean-it'."}
|
||||
{'name': 'yes-i-really-mean-it', 'required': True, 'helptext': "Please confirm that 'yes-i-really-mean-it'."}
|
||||
])
|
||||
@Authenticator
|
||||
def delete(self, osdid, reqargs):
|
||||
@ -3175,7 +3260,7 @@ class API_Storage_Ceph_OSD_State(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'state', 'choices': ('in', 'out'), 'required': True, 'helpmsg': "A valid state must be specified."},
|
||||
{'name': 'state', 'choices': ('in', 'out'), 'required': True, 'helptext': "A valid state must be specified."},
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, osdid, reqargs):
|
||||
@ -3295,9 +3380,9 @@ class API_Storage_Ceph_Pool_Root(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'pool', 'required': True, 'helpmsg': "A pool name must be specified."},
|
||||
{'name': 'pgs', 'required': True, 'helpmsg': "A placement group count must be specified."},
|
||||
{'name': 'replcfg', 'required': True, 'helpmsg': "A valid replication configuration must be specified."}
|
||||
{'name': 'pool', 'required': True, 'helptext': "A pool name must be specified."},
|
||||
{'name': 'pgs', 'required': True, 'helptext': "A placement group count must be specified."},
|
||||
{'name': 'replcfg', 'required': True, 'helptext': "A valid replication configuration must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, reqargs):
|
||||
@ -3370,8 +3455,8 @@ class API_Storage_Ceph_Pool_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'pgs', 'required': True, 'helpmsg': "A placement group count must be specified."},
|
||||
{'name': 'replcfg', 'required': True, 'helpmsg': "A valid replication configuration must be specified."}
|
||||
{'name': 'pgs', 'required': True, 'helptext': "A placement group count must be specified."},
|
||||
{'name': 'replcfg', 'required': True, 'helptext': "A valid replication configuration must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, pool, reqargs):
|
||||
@ -3415,7 +3500,7 @@ class API_Storage_Ceph_Pool_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'yes-i-really-mean-it', 'required': True, 'helpmsg': "Please confirm that 'yes-i-really-mean-it'."}
|
||||
{'name': 'yes-i-really-mean-it', 'required': True, 'helptext': "Please confirm that 'yes-i-really-mean-it'."}
|
||||
])
|
||||
@Authenticator
|
||||
def delete(self, pool, reqargs):
|
||||
@ -3559,9 +3644,9 @@ class API_Storage_Ceph_Volume_Root(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'volume', 'required': True, 'helpmsg': "A volume name must be specified."},
|
||||
{'name': 'pool', 'required': True, 'helpmsg': "A valid pool name must be specified."},
|
||||
{'name': 'size', 'required': True, 'helpmsg': "A volume size in bytes (or with k/M/G/T suffix) must be specified."}
|
||||
{'name': 'volume', 'required': True, 'helptext': "A volume name must be specified."},
|
||||
{'name': 'pool', 'required': True, 'helptext': "A valid pool name must be specified."},
|
||||
{'name': 'size', 'required': True, 'helptext': "A volume size in bytes (or with k/M/G/T suffix) must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, reqargs):
|
||||
@ -3635,7 +3720,7 @@ class API_Storage_Ceph_Volume_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'size', 'required': True, 'helpmsg': "A volume size in bytes (or with k/M/G/T suffix) must be specified."}
|
||||
{'name': 'size', 'required': True, 'helptext': "A volume size in bytes (or with k/M/G/T suffix) must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, pool, volume, reqargs):
|
||||
@ -3761,7 +3846,7 @@ api.add_resource(API_Storage_Ceph_Volume_Element, '/storage/ceph/volume/<pool>/<
|
||||
# /storage/ceph/volume/<pool>/<volume>/clone
|
||||
class API_Storage_Ceph_Volume_Element_Clone(Resource):
|
||||
@RequestParser([
|
||||
{'name': 'new_volume', 'required': True, 'helpmsg': "A new volume name must be specified."}
|
||||
{'name': 'new_volume', 'required': True, 'helptext': "A new volume name must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, pool, volume, reqargs):
|
||||
@ -3806,7 +3891,7 @@ api.add_resource(API_Storage_Ceph_Volume_Element_Clone, '/storage/ceph/volume/<p
|
||||
# /storage/ceph/volume/<pool>/<volume>/upload
|
||||
class API_Storage_Ceph_Volume_Element_Upload(Resource):
|
||||
@RequestParser([
|
||||
{'name': 'image_format', 'required': True, 'location': ['args'], 'helpmsg': "A source image format must be specified."}
|
||||
{'name': 'image_format', 'required': True, 'location': ['args'], 'helptext': "A source image format must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, pool, volume, reqargs):
|
||||
@ -3916,9 +4001,9 @@ class API_Storage_Ceph_Snapshot_Root(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'snapshot', 'required': True, 'helpmsg': "A snapshot name must be specified."},
|
||||
{'name': 'volume', 'required': True, 'helpmsg': "A volume name must be specified."},
|
||||
{'name': 'pool', 'required': True, 'helpmsg': "A pool name must be specified."}
|
||||
{'name': 'snapshot', 'required': True, 'helptext': "A snapshot name must be specified."},
|
||||
{'name': 'volume', 'required': True, 'helptext': "A volume name must be specified."},
|
||||
{'name': 'pool', 'required': True, 'helptext': "A pool name must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, reqargs):
|
||||
@ -4039,7 +4124,7 @@ class API_Storage_Ceph_Snapshot_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'new_name', 'required': True, 'helpmsg': "A new name must be specified."}
|
||||
{'name': 'new_name', 'required': True, 'helptext': "A new name must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def put(self, pool, volume, snapshot, reqargs):
|
||||
@ -4243,11 +4328,11 @@ class API_Provisioner_Template_System_Root(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'name', 'required': True, 'helpmsg': "A name must be specified."},
|
||||
{'name': 'vcpus', 'required': True, 'helpmsg': "A vcpus value must be specified."},
|
||||
{'name': 'vram', 'required': True, 'helpmsg': "A vram value in MB must be specified."},
|
||||
{'name': 'serial', 'required': True, 'helpmsg': "A serial value must be specified."},
|
||||
{'name': 'vnc', 'required': True, 'helpmsg': "A vnc value must be specified."},
|
||||
{'name': 'name', 'required': True, 'helptext': "A name must be specified."},
|
||||
{'name': 'vcpus', 'required': True, 'helptext': "A vcpus value must be specified."},
|
||||
{'name': 'vram', 'required': True, 'helptext': "A vram value in MB must be specified."},
|
||||
{'name': 'serial', 'required': True, 'helptext': "A serial value must be specified."},
|
||||
{'name': 'vnc', 'required': True, 'helptext': "A vnc value must be specified."},
|
||||
{'name': 'vnc_bind'},
|
||||
{'name': 'node_limit'},
|
||||
{'name': 'node_selector'},
|
||||
@ -4392,10 +4477,10 @@ class API_Provisioner_Template_System_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'vcpus', 'required': True, 'helpmsg': "A vcpus value must be specified."},
|
||||
{'name': 'vram', 'required': True, 'helpmsg': "A vram value in MB must be specified."},
|
||||
{'name': 'serial', 'required': True, 'helpmsg': "A serial value must be specified."},
|
||||
{'name': 'vnc', 'required': True, 'helpmsg': "A vnc value must be specified."},
|
||||
{'name': 'vcpus', 'required': True, 'helptext': "A vcpus value must be specified."},
|
||||
{'name': 'vram', 'required': True, 'helptext': "A vram value in MB must be specified."},
|
||||
{'name': 'serial', 'required': True, 'helptext': "A serial value must be specified."},
|
||||
{'name': 'vnc', 'required': True, 'helptext': "A vnc value must be specified."},
|
||||
{'name': 'vnc_bind'},
|
||||
{'name': 'node_limit'},
|
||||
{'name': 'node_selector'},
|
||||
@ -4674,7 +4759,7 @@ class API_Provisioner_Template_Network_Root(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'name', 'required': True, 'helpmsg': "A template name must be specified."},
|
||||
{'name': 'name', 'required': True, 'helptext': "A template name must be specified."},
|
||||
{'name': 'mac_template'}
|
||||
])
|
||||
@Authenticator
|
||||
@ -4833,7 +4918,7 @@ class API_Provisioner_Template_Network_Net_Root(Resource):
|
||||
return {'message': 'Template not found.'}, 404
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'vni', 'required': True, 'helpmsg': "A valid VNI must be specified."}
|
||||
{'name': 'vni', 'required': True, 'helptext': "A valid VNI must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, template, reqargs):
|
||||
@ -5026,7 +5111,7 @@ class API_Provisioner_Template_Storage_Root(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'name', 'required': True, 'helpmsg': "A template name must be specified."}
|
||||
{'name': 'name', 'required': True, 'helptext': "A template name must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, reqargs):
|
||||
@ -5171,8 +5256,8 @@ class API_Provisioner_Template_Storage_Disk_Root(Resource):
|
||||
return {'message': 'Template not found.'}, 404
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'disk_id', 'required': True, 'helpmsg': "A disk identifier in sdX or vdX format must be specified."},
|
||||
{'name': 'pool', 'required': True, 'helpmsg': "A storage pool must be specified."},
|
||||
{'name': 'disk_id', 'required': True, 'helptext': "A disk identifier in sdX or vdX format must be specified."},
|
||||
{'name': 'pool', 'required': True, 'helptext': "A storage pool must be specified."},
|
||||
{'name': 'source_volume'},
|
||||
{'name': 'disk_size'},
|
||||
{'name': 'filesystem'},
|
||||
@ -5279,7 +5364,7 @@ class API_Provisioner_Template_Storage_Disk_Element(Resource):
|
||||
abort(404)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'pool', 'required': True, 'helpmsg': "A storage pool must be specified."},
|
||||
{'name': 'pool', 'required': True, 'helptext': "A storage pool must be specified."},
|
||||
{'name': 'source_volume'},
|
||||
{'name': 'disk_size'},
|
||||
{'name': 'filesystem'},
|
||||
@ -5421,8 +5506,8 @@ class API_Provisioner_Userdata_Root(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'name', 'required': True, 'helpmsg': "A name must be specified."},
|
||||
{'name': 'data', 'required': True, 'helpmsg': "A userdata document must be specified."}
|
||||
{'name': 'name', 'required': True, 'helptext': "A name must be specified."},
|
||||
{'name': 'data', 'required': True, 'helptext': "A userdata document must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, reqargs):
|
||||
@ -5489,7 +5574,7 @@ class API_Provisioner_Userdata_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'data', 'required': True, 'helpmsg': "A userdata document must be specified."}
|
||||
{'name': 'data', 'required': True, 'helptext': "A userdata document must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, userdata, reqargs):
|
||||
@ -5522,7 +5607,7 @@ class API_Provisioner_Userdata_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'data', 'required': True, 'helpmsg': "A userdata document must be specified."}
|
||||
{'name': 'data', 'required': True, 'helptext': "A userdata document must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def put(self, userdata, reqargs):
|
||||
@ -5626,8 +5711,8 @@ class API_Provisioner_Script_Root(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'name', 'required': True, 'helpmsg': "A script name must be specified."},
|
||||
{'name': 'data', 'required': True, 'helpmsg': "A script document must be specified."}
|
||||
{'name': 'name', 'required': True, 'helptext': "A script name must be specified."},
|
||||
{'name': 'data', 'required': True, 'helptext': "A script document must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, reqargs):
|
||||
@ -5694,7 +5779,7 @@ class API_Provisioner_Script_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'data', 'required': True, 'helpmsg': "A script document must be specified."}
|
||||
{'name': 'data', 'required': True, 'helptext': "A script document must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, script, reqargs):
|
||||
@ -5727,7 +5812,7 @@ class API_Provisioner_Script_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'data', 'required': True, 'helpmsg': "A script document must be specified."}
|
||||
{'name': 'data', 'required': True, 'helptext': "A script document must be specified."}
|
||||
])
|
||||
@Authenticator
|
||||
def put(self, script, reqargs):
|
||||
@ -5849,9 +5934,9 @@ class API_Provisioner_OVA_Root(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'pool', 'required': True, 'location': ['args'], 'helpmsg': "A storage pool must be specified."},
|
||||
{'name': 'name', 'required': True, 'location': ['args'], 'helpmsg': "A VM name must be specified."},
|
||||
{'name': 'ova_size', 'required': True, 'location': ['args'], 'helpmsg': "An OVA size must be specified."},
|
||||
{'name': 'pool', 'required': True, 'location': ['args'], 'helptext': "A storage pool must be specified."},
|
||||
{'name': 'name', 'required': True, 'location': ['args'], 'helptext': "A VM name must be specified."},
|
||||
{'name': 'ova_size', 'required': True, 'location': ['args'], 'helptext': "An OVA size must be specified."},
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, reqargs):
|
||||
@ -5926,8 +6011,8 @@ class API_Provisioner_OVA_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'pool', 'required': True, 'location': ['args'], 'helpmsg': "A storage pool must be specified."},
|
||||
{'name': 'ova_size', 'required': True, 'location': ['args'], 'helpmsg': "An OVA size must be specified."},
|
||||
{'name': 'pool', 'required': True, 'location': ['args'], 'helptext': "A storage pool must be specified."},
|
||||
{'name': 'ova_size', 'required': True, 'location': ['args'], 'helptext': "An OVA size must be specified."},
|
||||
])
|
||||
@Authenticator
|
||||
def post(self, ova, reqargs):
|
||||
@ -6056,8 +6141,8 @@ class API_Provisioner_Profile_Root(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'name', 'required': True, 'helpmsg': "A profile name must be specified."},
|
||||
{'name': 'profile_type', 'required': True, 'helpmsg': "A profile type must be specified."},
|
||||
{'name': 'name', 'required': True, 'helptext': "A profile name must be specified."},
|
||||
{'name': 'profile_type', 'required': True, 'helptext': "A profile type must be specified."},
|
||||
{'name': 'system_template'},
|
||||
{'name': 'network_template'},
|
||||
{'name': 'storage_template'},
|
||||
@ -6175,7 +6260,7 @@ class API_Provisioner_Profile_Element(Resource):
|
||||
)
|
||||
|
||||
@RequestParser([
|
||||
{'name': 'profile_type', 'required': True, 'helpmsg': "A profile type must be specified."},
|
||||
{'name': 'profile_type', 'required': True, 'helptext': "A profile type must be specified."},
|
||||
{'name': 'system_template'},
|
||||
{'name': 'network_template'},
|
||||
{'name': 'storage_template'},
|
||||
@ -6357,8 +6442,8 @@ api.add_resource(API_Provisioner_Profile_Element, '/provisioner/profile/<profile
|
||||
# /provisioner/create
|
||||
class API_Provisioner_Create_Root(Resource):
|
||||
@RequestParser([
|
||||
{'name': 'name', 'required': True, 'helpmsg': "A VM name must be specified."},
|
||||
{'name': 'profile', 'required': True, 'helpmsg': "A profile name must be specified."},
|
||||
{'name': 'name', 'required': True, 'helptext': "A VM name must be specified."},
|
||||
{'name': 'profile', 'required': True, 'helptext': "A profile name must be specified."},
|
||||
{'name': 'define_vm'},
|
||||
{'name': 'start_vm'},
|
||||
{'name': 'arg', 'action': 'append'}
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -31,10 +31,55 @@ def initialize(config):
|
||||
Initialize the PVC cluster
|
||||
|
||||
API endpoint: GET /api/v1/initialize
|
||||
API arguments: yes-i-really-mean-it
|
||||
API schema: {json_data_object}
|
||||
"""
|
||||
params = {
|
||||
'yes-i-really-mean-it': 'yes'
|
||||
}
|
||||
response = call_api(config, 'post', '/initialize', params=params)
|
||||
|
||||
if response.status_code == 200:
|
||||
retstatus = True
|
||||
else:
|
||||
retstatus = False
|
||||
|
||||
return retstatus, response.json().get('message', '')
|
||||
|
||||
|
||||
def backup(config):
|
||||
"""
|
||||
Get a JSON backup of the cluster
|
||||
|
||||
API endpoint: GET /api/v1/backup
|
||||
API arguments:
|
||||
API schema: {json_data_object}
|
||||
"""
|
||||
response = call_api(config, 'post', '/initialize')
|
||||
response = call_api(config, 'get', '/backup')
|
||||
|
||||
if response.status_code == 200:
|
||||
return True, response.json()
|
||||
else:
|
||||
return False, response.json().get('message', '')
|
||||
|
||||
|
||||
def restore(config, cluster_data):
|
||||
"""
|
||||
Restore a JSON backup to the cluster
|
||||
|
||||
API endpoint: POST /api/v1/restore
|
||||
API arguments: yes-i-really-mean-it
|
||||
API schema: {json_data_object}
|
||||
"""
|
||||
cluster_data_json = json.dumps(cluster_data)
|
||||
|
||||
params = {
|
||||
'yes-i-really-mean-it': 'yes'
|
||||
}
|
||||
data = {
|
||||
'cluster_data': cluster_data_json
|
||||
}
|
||||
response = call_api(config, 'post', '/restore', params=params, data=data)
|
||||
|
||||
if response.status_code == 200:
|
||||
retstatus = True
|
||||
@ -115,7 +160,7 @@ def format_info(cluster_information, oformat):
|
||||
if cluster_information['storage_health_msg']:
|
||||
for line in cluster_information['storage_health_msg']:
|
||||
ainformation.append(' > {}'.format(line))
|
||||
|
||||
|
||||
return '\n'.join(ainformation)
|
||||
|
||||
ainformation.append('{}PVC cluster status:{}'.format(ansiprint.bold(), ansiprint.end()))
|
||||
|
@ -2721,14 +2721,14 @@ def provisioner_template_system_list(limit):
|
||||
help='The amount of vRAM (in MB).'
|
||||
)
|
||||
@click.option(
|
||||
'-s', '--serial', 'serial',
|
||||
'-s/-S', '--serial/--no-serial', 'serial',
|
||||
is_flag=True, default=False,
|
||||
help='Enable the virtual serial console.'
|
||||
)
|
||||
@click.option(
|
||||
'-n', '--vnc', 'vnc',
|
||||
'-n/-N', '--vnc/--no-vnc', 'vnc',
|
||||
is_flag=True, default=False,
|
||||
help='Enable the VNC console.'
|
||||
help='Enable/disable the VNC console.'
|
||||
)
|
||||
@click.option(
|
||||
'-b', '--vnc-bind', 'vnc_bind',
|
||||
@ -2801,14 +2801,14 @@ def provisioner_template_system_add(name, vcpus, vram, serial, vnc, vnc_bind, no
|
||||
help='The amount of vRAM (in MB).'
|
||||
)
|
||||
@click.option(
|
||||
'-s', '--serial', 'serial',
|
||||
'-s/-S', '--serial/--no-serial', 'serial',
|
||||
is_flag=True, default=None,
|
||||
help='Enable the virtual serial console.'
|
||||
)
|
||||
@click.option(
|
||||
'-n', '--vnc', 'vnc',
|
||||
'-n/-N', '--vnc/--no-vnc', 'vnc',
|
||||
is_flag=True, default=None,
|
||||
help='Enable the VNC console.'
|
||||
help='Enable/disable the VNC console.'
|
||||
)
|
||||
@click.option(
|
||||
'-b', '--vnc-bind', 'vnc_bind',
|
||||
@ -4078,6 +4078,71 @@ def status_cluster(oformat):
|
||||
cleanup(retcode, retdata)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# pvc task
|
||||
###############################################################################
|
||||
@click.group(name='task', short_help='Perform PVC cluster tasks.', context_settings=CONTEXT_SETTINGS)
|
||||
def cli_task():
|
||||
"""
|
||||
Perform administrative tasks against the PVC cluster.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
###############################################################################
|
||||
# pvc task backup
|
||||
###############################################################################
|
||||
@click.command(name='backup', short_help='Create JSON backup of cluster.')
|
||||
@click.option(
|
||||
'-f', '--file', 'filename',
|
||||
default=None, type=click.File(),
|
||||
help='Write backup data to this file.'
|
||||
)
|
||||
@cluster_req
|
||||
def task_backup(filename):
|
||||
"""
|
||||
Create a JSON-format backup of the cluster Zookeeper database.
|
||||
"""
|
||||
|
||||
retcode, retdata = pvc_cluster.backup(config)
|
||||
if filename:
|
||||
with open(filename, 'wb') as fh:
|
||||
fh.write(retdata)
|
||||
retdata = 'Data written to {}'.format(filename)
|
||||
cleanup(retcode, retdata)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# pvc task restore
|
||||
###############################################################################
|
||||
@click.command(name='restore', short_help='Restore JSON backup to cluster.')
|
||||
@click.option(
|
||||
'-f', '--file', 'filename',
|
||||
required=True, default=None, type=click.File(),
|
||||
help='Read backup data from this file.'
|
||||
)
|
||||
@click.option(
|
||||
'-y', '--yes', 'confirm_flag',
|
||||
is_flag=True, default=False,
|
||||
help='Confirm the restore'
|
||||
)
|
||||
@cluster_req
|
||||
def task_restore(filename, confirm_flag):
|
||||
"""
|
||||
Restore the JSON backup data from a file to the cluster.
|
||||
"""
|
||||
|
||||
if not confirm_flag:
|
||||
try:
|
||||
click.confirm('Replace all existing cluster data from coordinators with backup file "{}"'.format(filename.name), prompt_suffix='? ', abort=True)
|
||||
except Exception:
|
||||
exit(0)
|
||||
|
||||
cluster_data = json.loads(filename.read())
|
||||
retcode, retmsg = pvc_cluster.restore(config, cluster_data)
|
||||
cleanup(retcode, retmsg)
|
||||
|
||||
|
||||
###############################################################################
|
||||
# pvc init
|
||||
###############################################################################
|
||||
@ -4085,10 +4150,10 @@ def status_cluster(oformat):
|
||||
@click.option(
|
||||
'-y', '--yes', 'confirm_flag',
|
||||
is_flag=True, default=False,
|
||||
help='Confirm the removal'
|
||||
help='Confirm the initialization'
|
||||
)
|
||||
@cluster_req
|
||||
def init_cluster(confirm_flag):
|
||||
def task_init(confirm_flag):
|
||||
"""
|
||||
Perform initialization of a new PVC cluster.
|
||||
"""
|
||||
@ -4323,6 +4388,10 @@ cli_provisioner.add_command(provisioner_status)
|
||||
cli_maintenance.add_command(maintenance_on)
|
||||
cli_maintenance.add_command(maintenance_off)
|
||||
|
||||
cli_task.add_command(task_backup)
|
||||
cli_task.add_command(task_restore)
|
||||
cli_task.add_command(task_init)
|
||||
|
||||
cli.add_command(cli_cluster)
|
||||
cli.add_command(cli_node)
|
||||
cli.add_command(cli_vm)
|
||||
@ -4330,8 +4399,8 @@ cli.add_command(cli_network)
|
||||
cli.add_command(cli_storage)
|
||||
cli.add_command(cli_provisioner)
|
||||
cli.add_command(cli_maintenance)
|
||||
cli.add_command(cli_task)
|
||||
cli.add_command(status_cluster)
|
||||
cli.add_command(init_cluster)
|
||||
|
||||
|
||||
#
|
||||
|
20
debian/changelog
vendored
20
debian/changelog
vendored
@ -1,3 +1,23 @@
|
||||
pvc (0.9.8-0) unstable; urgency=high
|
||||
|
||||
* Adds support for cluster backup/restore
|
||||
* Moves location of `init` command in CLI to make room for the above
|
||||
* Cleans up some invalid help messages from the API
|
||||
|
||||
-- Joshua M. Boniface <joshua@boniface.me> Tue, 24 Nov 2020 12:26:57 -0500
|
||||
|
||||
pvc (0.9.7-0) unstable; urgency=high
|
||||
|
||||
* Fixes bug with provisioner system template modifications
|
||||
|
||||
-- Joshua M. Boniface <joshua@boniface.me> Thu, 19 Nov 2020 10:48:28 -0500
|
||||
|
||||
pvc (0.9.6-0) unstable; urgency=high
|
||||
|
||||
* Fixes bug with migrations
|
||||
|
||||
-- Joshua M. Boniface <joshua@boniface.me> Tue, 17 Nov 2020 13:01:54 -0500
|
||||
|
||||
pvc (0.9.5-0) unstable; urgency=high
|
||||
|
||||
* Fixes bug with line count in log follow
|
||||
|
@ -18,6 +18,20 @@ To get started with PVC, please see the [About](https://parallelvirtualcluster.r
|
||||
|
||||
## Changelog
|
||||
|
||||
#### v0.9.8
|
||||
|
||||
* Adds support for cluster backup/restore
|
||||
* Moves location of `init` command in CLI to make room for the above
|
||||
* Cleans up some invalid help messages from the API
|
||||
|
||||
#### v0.9.7
|
||||
|
||||
* Fixes bug with provisioner system template modifications
|
||||
|
||||
#### v0.9.6
|
||||
|
||||
* Fixes bug with migrations
|
||||
|
||||
#### v0.9.5
|
||||
|
||||
* Fixes bug with line count in log follow
|
||||
|
@ -54,7 +54,7 @@ import pvcnoded.CephInstance as CephInstance
|
||||
import pvcnoded.MetadataAPIInstance as MetadataAPIInstance
|
||||
|
||||
# Version string for startup output
|
||||
version = '0.9.5'
|
||||
version = '0.9.8'
|
||||
|
||||
###############################################################################
|
||||
# PVCD - node daemon startup program
|
||||
|
@ -381,6 +381,7 @@ class VMInstance(object):
|
||||
})
|
||||
migrate_lock_node.release()
|
||||
migrate_lock_state.release()
|
||||
self.inmigrate = False
|
||||
self.logger.out('Aborted migration: {}'.format(reason), state='i', prefix='Domain {}'.format(self.domuuid))
|
||||
|
||||
# Acquire exclusive lock on the domain node key
|
||||
|
Reference in New Issue
Block a user