#!/usr/bin/env python3 # Daemon.py - PVC HTTP API daemon # Part of the Parallel Virtual Cluster (PVC) system # # Copyright (C) 2018-2021 Joshua M. Boniface # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, version 3. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # ############################################################################### import flask from functools import wraps from flask_restful import Resource, Api, reqparse, abort from celery import Celery from pvcapid.Daemon import config, strtobool, API_VERSION import pvcapid.helper as api_helper import pvcapid.provisioner as api_provisioner import pvcapid.benchmark as api_benchmark import pvcapid.ova as api_ova from flask_sqlalchemy import SQLAlchemy # Create Flask app and set config values app = flask.Flask(__name__) app.config['CELERY_BROKER_URL'] = 'redis://{}:{}{}'.format(config['queue_host'], config['queue_port'], config['queue_path']) app.config['CELERY_RESULT_BACKEND'] = 'redis://{}:{}{}'.format(config['queue_host'], config['queue_port'], config['queue_path']) app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://{}:{}@{}:{}/{}'.format(config['database_user'], config['database_password'], config['database_host'], config['database_port'], config['database_name']) if config['debug']: app.config['DEBUG'] = True else: app.config['DEBUG'] = False if config['auth_enabled']: app.config["SECRET_KEY"] = config['auth_secret_key'] # Create SQLAlchemy database db = SQLAlchemy(app) # Create Flask blueprint blueprint = flask.Blueprint('api', __name__, url_prefix='/api/v1') # Create Flask-RESTful definition api = Api(blueprint) app.register_blueprint(blueprint) # Create celery definition celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL']) celery.conf.update(app.config) # # Custom decorators # # Request parser decorator class RequestParser(object): def __init__(self, reqargs): self.reqargs = reqargs def __call__(self, function): if not callable(function): return @wraps(function) def wrapped_function(*args, **kwargs): parser = reqparse.RequestParser() # Parse and add each argument for reqarg in self.reqargs: location = reqarg.get('location', None) if location is None: location = ['args', 'form'] parser.add_argument( reqarg.get('name', None), required=reqarg.get('required', False), action=reqarg.get('action', None), choices=reqarg.get('choices', ()), help=reqarg.get('helptext', None), location=location ) reqargs = parser.parse_args() kwargs['reqargs'] = reqargs return function(*args, **kwargs) return wrapped_function # Authentication decorator function def Authenticator(function): @wraps(function) def authenticate(*args, **kwargs): # No authentication required if not config['auth_enabled']: return function(*args, **kwargs) # Session-based authentication if 'token' in flask.session: return function(*args, **kwargs) # Key header-based authentication if 'X-Api-Key' in flask.request.headers: if any(token for token in config['auth_tokens'] if flask.request.headers.get('X-Api-Key') == token.get('token')): return function(*args, **kwargs) else: return {"message": "X-Api-Key Authentication failed."}, 401 # All authentications failed return {"message": "X-Api-Key Authentication required."}, 401 return authenticate # # Job functions # @celery.task(bind=True) def create_vm(self, vm_name, profile_name, define_vm=True, start_vm=True, script_run_args=[]): return api_provisioner.create_vm(self, vm_name, profile_name, define_vm=define_vm, start_vm=start_vm, script_run_args=script_run_args) @celery.task(bind=True) def run_benchmark(self, pool): return api_benchmark.run_benchmark(self, pool) ########################################################## # API Root/Authentication ########################################################## # / class API_Root(Resource): def get(self): """ Return the PVC API version string --- tags: - root responses: 200: description: OK schema: type: object id: API-Version properties: message: type: string description: A text message example: "PVC API version 1.0" """ return {"message": "PVC API version {}".format(API_VERSION)} api.add_resource(API_Root, '/') # /doc - NOTE: Until flask_swagger is packaged for Debian this must be disabled # class API_Doc(Resource): # def get(self): # """ # Provide the Swagger API documentation # --- # tags: # - root # responses: # 200: # description: OK # """ # swagger_data = swagger(pvc_api.app) # swagger_data['info']['version'] = API_VERSION # swagger_data['info']['title'] = "PVC Client and Provisioner API" # swagger_data['host'] = "{}:{}".format(config['listen_address'], config['listen_port']) # return swagger_data # # # api.add_resource(API_Doc, '/doc') # /login class API_Login(Resource): def post(self): """ Log in to the PVC API with an authentication key --- tags: - root parameters: - in: query name: token type: string required: true responses: 200: description: OK schema: type: object id: Message properties: message: type: string description: A text message 302: description: Authentication disabled 401: description: Unauthorized schema: type: object id: Message """ if not config['auth_enabled']: return flask.redirect(Api.url_for(api, API_Root)) if any(token for token in config['auth_tokens'] if flask.request.values['token'] in token['token']): flask.session['token'] = flask.request.form['token'] return {"message": "Authentication successful"}, 200 else: {"message": "Authentication failed"}, 401 api.add_resource(API_Login, '/login') # /logout class API_Logout(Resource): def post(self): """ Log out of an existing PVC API session --- tags: - root responses: 200: description: OK schema: type: object id: Message 302: description: Authentication disabled """ if not config['auth_enabled']: return flask.redirect(Api.url_for(api, API_Root)) flask.session.pop('token', None) return {"message": "Deauthentication successful"}, 200 api.add_resource(API_Logout, '/logout') # /initialize class API_Initialize(Resource): @RequestParser([ {'name': 'overwrite', 'required': False}, {'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, reqargs): """ Initialize a new PVC cluster If the 'overwrite' option is not True, the cluster will return 400 if the `/primary_node` key is found. If 'overwrite' is True, the existing cluster data will be erased and new, empty data written in its place. All node daemons should be stopped before running this command, and the API daemon started manually to avoid undefined behavior. --- tags: - root parameters: - in: query name: overwrite type: bool required: false description: A flag to enable or disable (default) overwriting existing data - 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 schema: type: object id: Message properties: message: type: string description: A text message 400: description: Bad request """ if reqargs.get('overwrite', False): overwrite_flag = True if api_helper.initialize_cluster(overwrite=overwrite_flag): return {"message": "Successfully initialized a new PVC cluster"}, 200 else: return {"message": "PVC cluster already initialized"}, 400 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 def get(self): """ Return the current PVC cluster status --- tags: - root responses: 200: description: OK schema: type: object id: ClusterStatus properties: health: type: string description: The overall cluster health example: Optimal storage_health: type: string description: The overall storage cluster health example: Optimal primary_node: type: string description: The current primary coordinator node example: pvchv1 upstream_ip: type: string description: The cluster upstream IP address in CIDR format example: 10.0.0.254/24 nodes: type: object properties: total: type: integer description: The total number of nodes in the cluster example: 3 state-combination: type: integer description: The total number of nodes in {state-combination} state, where {state-combination} is the node daemon and domain states in CSV format, e.g. "run,ready", "stop,flushed", etc. vms: type: object properties: total: type: integer description: The total number of VMs in the cluster example: 6 state: type: integer description: The total number of VMs in {state} state, e.g. "start", "stop", etc. networks: type: integer description: The total number of networks in the cluster osds: type: object properties: total: type: integer description: The total number of OSDs in the storage cluster example: 3 state-combination: type: integer description: The total number of OSDs in {state-combination} state, where {state-combination} is the OSD up and in states in CSV format, e.g. "up,in", "down,out", etc. pools: type: integer description: The total number of pools in the storage cluster volumes: type: integer description: The total number of volumes in the storage cluster snapshots: type: integer description: The total number of snapshots in the storage cluster 400: description: Bad request """ return api_helper.cluster_status() @RequestParser([ {'name': 'state', 'choices': ('true', 'false'), 'required': True, 'helptext': "A valid state must be specified."} ]) @Authenticator def post(self, reqargs): """ Set the cluster maintenance mode --- tags: - node parameters: - in: query name: state type: boolean required: true description: The cluster maintenance state responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.cluster_maintenance(reqargs.get('state', 'false')) api.add_resource(API_Status, '/status') ########################################################## # Client API - Node ########################################################## # /node class API_Node_Root(Resource): @RequestParser([ {'name': 'limit'}, {'name': 'daemon_state'}, {'name': 'coordinator_state'}, {'name': 'domain_state'} ]) @Authenticator def get(self, reqargs): """ Return a list of nodes in the cluster --- tags: - node definitions: - schema: type: object id: node properties: name: type: string description: The name of the node daemon_state: type: string description: The current daemon state coordinator_state: type: string description: The current coordinator state domain_state: type: string description: The current domain (VM) state cpu_count: type: integer description: The number of available CPU cores kernel: type: string desription: The running kernel version from uname os: type: string description: The current operating system type arch: type: string description: The architecture of the CPU load: type: number format: float description: The current 5-minute CPU load domains_count: type: integer description: The number of running domains (VMs) running_domains: type: string description: The list of running domains (VMs) by UUID vcpu: type: object properties: total: type: integer description: The total number of real CPU cores available allocated: type: integer description: The total number of allocated vCPU cores memory: type: object properties: total: type: integer description: The total amount of node RAM in MB used: type: integer description: The total used RAM on the node in MB free: type: integer description: The total free RAM on the node in MB allocated: type: integer description: The total amount of RAM allocated to running domains in MB provisioned: type: integer description: The total amount of RAM provisioned to all domains (regardless of state) on this node in MB parameters: - in: query name: limit type: string required: false description: A search limit; fuzzy by default, use ^/$ to force exact matches - in: query name: daemon_state type: string required: false description: Limit results to nodes in the specified daemon state - in: query name: coordinator_state type: string required: false description: Limit results to nodes in the specified coordinator state - in: query name: domain_state type: string required: false description: Limit results to nodes in the specified domain state responses: 200: description: OK schema: type: array items: $ref: '#/definitions/node' """ return api_helper.node_list( limit=reqargs.get('limit', None), daemon_state=reqargs.get('daemon_state', None), coordinator_state=reqargs.get('coordinator_state', None), domain_state=reqargs.get('domain_state', None) ) api.add_resource(API_Node_Root, '/node') # /node/ class API_Node_Element(Resource): @Authenticator def get(self, node): """ Return information about {node} --- tags: - node responses: 200: description: OK schema: $ref: '#/definitions/node' 404: description: Not found schema: type: object id: Message """ return api_helper.node_list(node, is_fuzzy=False) api.add_resource(API_Node_Element, '/node/') # /node//daemon-state class API_Node_DaemonState(Resource): @Authenticator def get(self, node): """ Return the daemon state of {node} --- tags: - node responses: 200: description: OK schema: type: object id: NodeDaemonState properties: name: type: string description: The name of the node daemon_state: type: string description: The current daemon state 404: description: Not found schema: type: object id: Message """ return api_helper.node_daemon_state(node) api.add_resource(API_Node_DaemonState, '/node//daemon-state') # /node//coordinator-state class API_Node_CoordinatorState(Resource): @Authenticator def get(self, node): """ Return the coordinator state of {node} --- tags: - node responses: 200: description: OK schema: type: object id: NodeCoordinatorState properties: name: type: string description: The name of the node coordinator_state: type: string description: The current coordinator state 404: description: Not found schema: type: object id: Message """ return api_helper.node_coordinator_state(node) @RequestParser([ {'name': 'state', 'choices': ('primary', 'secondary'), 'helptext': "A valid state must be specified", 'required': True} ]) @Authenticator def post(self, node, reqargs): """ Set the coordinator state of {node} --- tags: - node parameters: - in: query name: action type: string required: true description: The new coordinator state of the node enum: - primary - secondary responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ if reqargs['state'] == 'primary': return api_helper.node_primary(node) if reqargs['state'] == 'secondary': return api_helper.node_secondary(node) abort(400) api.add_resource(API_Node_CoordinatorState, '/node//coordinator-state') # /node//domain-state class API_Node_DomainState(Resource): @Authenticator def get(self, node): """ Return the domain state of {node} --- tags: - node responses: 200: description: OK schema: type: object id: NodeDomainState properties: name: type: string description: The name of the node domain_state: type: string description: The current domain state 404: description: Not found schema: type: object id: Message """ return api_helper.node_domain_state(node) @RequestParser([ {'name': 'state', 'choices': ('ready', 'flush'), 'helptext': "A valid state must be specified", 'required': True}, {'name': 'wait'} ]) @Authenticator def post(self, node, reqargs): """ Set the domain state of {node} --- tags: - node parameters: - in: query name: action type: string required: true description: The new domain state of the node enum: - flush - ready - in: query name: wait type: boolean description: Whether to block waiting for the full flush/ready state responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ if reqargs['state'] == 'flush': return api_helper.node_flush(node, bool(strtobool(reqargs.get('wait', 'false')))) if reqargs['state'] == 'ready': return api_helper.node_ready(node, bool(strtobool(reqargs.get('wait', 'false')))) abort(400) api.add_resource(API_Node_DomainState, '/node//domain-state') ########################################################## # Client API - VM ########################################################## # /vm class API_VM_Root(Resource): @RequestParser([ {'name': 'limit'}, {'name': 'node'}, {'name': 'state'}, ]) @Authenticator def get(self, reqargs): """ Return a list of VMs in the cluster --- tags: - vm definitions: - schema: type: object id: vm properties: name: type: string description: The name of the VM uuid: type: string description: The UUID of the VM state: type: string description: The current state of the VM node: type: string description: The node the VM is currently assigned to last_node: type: string description: The last node the VM was assigned to before migrating migrated: type: string description: Whether the VM has been migrated, either "no" or "from " failed_reason: type: string description: Information about why the VM failed to start node_limit: type: array description: The node(s) the VM is permitted to be assigned to items: type: string node_selector: type: string description: The selector used to determine candidate nodes during migration node_autostart: type: boolean description: Whether to autostart the VM when its node returns to ready domain state migration_method: type: string description: The preferred migration method (live, shutdown, none) description: type: string description: The description of the VM profile: type: string description: The provisioner profile used to create the VM memory: type: integer description: The assigned RAM of the VM in MB memory_stats: type: object properties: actual: type: integer description: The total active memory of the VM in kB swap_in: type: integer description: The amount of swapped in data in kB swap_out: type: integer description: The amount of swapped out data in kB major_fault: type: integer description: The number of major page faults minor_fault: type: integer description: The number of minor page faults unused: type: integer description: The amount of memory left completely unused by the system in kB available: type: integer description: The total amount of usable memory as seen by the domain in kB usable: type: integer description: How much the balloon can be inflated without pushing the guest system to swap in kB last_update: type: integer description: Timestamp of the last update of statistics, in seconds rss: type: integer description: The Resident Set Size of the process running the domain in kB vcpu: type: integer description: The assigned vCPUs of the VM vcpu_topology: type: string description: The topology of the assigned vCPUs in Sockets/Cores/Threads format vcpu_stats: type: object properties: cpu_time: type: integer description: The active CPU time for all vCPUs user_time: type: integer description: vCPU user time system_time: type: integer description: vCPU system time type: type: string description: The type of the VM arch: type: string description: The architecture of the VM machine: type: string description: The QEMU machine type of the VM console: type: string descritpion: The serial console type of the VM vnc: type: object properties: listen: type: string description: The active VNC listen address or 'None' port: type: string description: The active VNC port or 'None' emulator: type: string description: The binary emulator of the VM features: type: array description: The available features of the VM items: type: string networks: type: array description: The PVC networks attached to the VM items: type: object properties: type: type: string description: The PVC network type mac: type: string description: The MAC address of the VM network interface source: type: string description: The parent network bridge on the node vni: type: integer description: The VNI (PVC network) of the network bridge model: type: string description: The virtual network device model rd_bytes: type: integer description: The number of read bytes on the interface rd_packets: type: integer description: The number of read packets on the interface rd_errors: type: integer description: The number of read errors on the interface rd_drops: type: integer description: The number of read drops on the interface wr_bytes: type: integer description: The number of write bytes on the interface wr_packets: type: integer description: The number of write packets on the interface wr_errors: type: integer description: The number of write errors on the interface wr_drops: type: integer description: The number of write drops on the interface disks: type: array description: The PVC storage volumes attached to the VM items: type: object properties: type: type: string description: The type of volume name: type: string description: The full name of the volume in "pool/volume" format dev: type: string description: The device ID of the volume in the VM bus: type: string description: The virtual bus of the volume in the VM rd_req: type: integer description: The number of read requests from the volume rd_bytes: type: integer description: The number of read bytes from the volume wr_req: type: integer description: The number of write requests to the volume wr_bytes: type: integer description: The number of write bytes to the volume controllers: type: array description: The device controllers attached to the VM items: type: object properties: type: type: string description: The type of the controller model: type: string description: The model of the controller xml: type: string description: The raw Libvirt XML definition of the VM parameters: - in: query name: limit type: string required: false description: A name search limit; fuzzy by default, use ^/$ to force exact matches - in: query name: node type: string required: false description: Limit list to VMs assigned to this node - in: query name: state type: string required: false description: Limit list to VMs in this state responses: 200: description: OK schema: type: array items: $ref: '#/definitions/vm' """ return api_helper.vm_list( reqargs.get('node', None), reqargs.get('state', None), reqargs.get('limit', None) ) @RequestParser([ {'name': 'limit'}, {'name': 'node'}, {'name': 'selector', 'choices': ('mem', 'vcpus', 'load', 'vms'), 'helptext': "A valid selector must be specified"}, {'name': 'autostart'}, {'name': 'migration_method', 'choices': ('live', 'shutdown', 'none'), 'helptext': "A valid migration_method must be specified"}, {'name': 'xml', 'required': True, 'helptext': "A Libvirt XML document must be specified"}, ]) @Authenticator def post(self, reqargs): """ Create a new virtual machine --- tags: - vm parameters: - in: query name: xml type: string required: true description: The raw Libvirt XML definition of the VM - in: query name: node type: string required: false description: The node the VM should be assigned to; autoselect if empty or invalid - in: query name: limit type: string required: false description: The CSV list of node(s) the VM is permitted to be assigned to; should include "node" and any other valid target nodes; this limit will be used for autoselection on definition and migration - in: query name: selector type: string required: false description: The selector used to determine candidate nodes during migration default: mem enum: - mem - vcpus - load - vms - in: query name: autostart type: boolean required: false description: Whether to autostart the VM when its node returns to ready domain state - in: query name: migration_method type: string required: false description: The preferred migration method (live, shutdown, none) default: none enum: - live - shutdown - none responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.vm_define( reqargs.get('xml'), reqargs.get('node', None), reqargs.get('limit', None), reqargs.get('selector', 'mem'), bool(strtobool(reqargs.get('autostart', 'false'))), reqargs.get('migration_method', 'none') ) api.add_resource(API_VM_Root, '/vm') # /vm/ class API_VM_Element(Resource): @Authenticator def get(self, vm): """ Return information about {vm} --- tags: - vm responses: 200: description: OK schema: $ref: '#/definitions/vm' 404: description: Not found schema: type: object id: Message """ return api_helper.vm_list(None, None, vm, is_fuzzy=False) @RequestParser([ {'name': 'limit'}, {'name': 'node'}, {'name': 'selector', 'choices': ('mem', 'vcpus', 'load', 'vms'), 'helptext': "A valid selector must be specified"}, {'name': 'autostart'}, {'name': 'migration_method', 'choices': ('live', 'shutdown', 'none'), 'helptext': "A valid migration_method must be specified"}, {'name': 'xml', 'required': True, 'helptext': "A Libvirt XML document must be specified"}, ]) @Authenticator def post(self, vm, reqargs): """ Create new {vm} Note: The name {vm} is ignored; only the "name" value from the Libvirt XML is used This endpoint is identical to "POST /api/v1/vm" --- tags: - vm parameters: - in: query name: xml type: string required: true description: The raw Libvirt XML definition of the VM - in: query name: node type: string required: false description: The node the VM should be assigned to; autoselect if empty or invalid - in: query name: limit type: string required: false description: The CSV list of node(s) the VM is permitted to be assigned to; should include "node" and any other valid target nodes; this limit will be used for autoselection on definition and migration - in: query name: selector type: string required: false description: The selector used to determine candidate nodes during migration default: mem enum: - mem - vcpus - load - vms - in: query name: autostart type: boolean required: false description: Whether to autostart the VM when its node returns to ready domain state - in: query name: migration_method type: string required: false description: The preferred migration method (live, shutdown, none) default: none enum: - live - shutdown - none responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.vm_define( reqargs.get('xml'), reqargs.get('node', None), reqargs.get('limit', None), reqargs.get('selector', 'mem'), bool(strtobool(reqargs.get('autostart', 'false'))), reqargs.get('migration_method', 'none') ) @RequestParser([ {'name': 'restart'}, {'name': 'xml', 'required': True, 'helptext': "A Libvirt XML document must be specified"}, ]) @Authenticator def put(self, vm, reqargs): """ Update the Libvirt XML of {vm} --- tags: - vm parameters: - in: query name: xml type: string required: true description: The raw Libvirt XML definition of the VM - in: query name: restart type: boolean description: Whether to automatically restart the VM to apply the new configuration responses: 200: description: OK schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ return api_helper.vm_modify( vm, bool(strtobool(reqargs.get('restart', 'false'))), reqargs.get('xml', None) ) @RequestParser([ {'name': 'delete_disks'}, ]) @Authenticator def delete(self, vm, reqargs): """ Remove {vm} --- tags: - vm parameters: - in: query name: delete_disks type: boolean default: false description: Whether to automatically delete all VM disk volumes responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message 404: description: VM not found schema: type: object id: Message """ if bool(strtobool(reqargs.get('delete_disks', 'false'))): return api_helper.vm_remove(vm) else: return api_helper.vm_undefine(vm) api.add_resource(API_VM_Element, '/vm/') # /vm//meta class API_VM_Metadata(Resource): @Authenticator def get(self, vm): """ Return the metadata of {vm} --- tags: - vm responses: 200: description: OK schema: type: object id: VMMetadata properties: name: type: string description: The name of the VM node_limit: type: array description: The node(s) the VM is permitted to be assigned to items: type: string node_selector: type: string description: The selector used to determine candidate nodes during migration node_autostart: type: string description: Whether to autostart the VM when its node returns to ready domain state migration_method: type: string description: The preferred migration method (live, shutdown, none) 404: description: Not found schema: type: object id: Message """ return api_helper.get_vm_meta(vm) @RequestParser([ {'name': 'limit'}, {'name': 'selector', 'choices': ('mem', 'vcpus', 'load', 'vms'), 'helptext': "A valid selector must be specified"}, {'name': 'autostart'}, {'name': 'profile'}, {'name': 'migration_method', 'choices': ('live', 'shutdown', 'none'), 'helptext': "A valid migration_method must be specified"}, ]) @Authenticator def post(self, vm, reqargs): """ Set the metadata of {vm} --- tags: - vm parameters: - in: query name: limit type: string required: false description: The CSV list of node(s) the VM is permitted to be assigned to; should include "node" and any other valid target nodes; this limit will be used for autoselection on definition and migration - in: query name: selector type: string required: false description: The selector used to determine candidate nodes during migration enum: - mem - vcpus - load - vms - in: query name: autostart type: boolean required: false description: Whether to autostart the VM when its node returns to ready domain state - in: query name: profile type: string required: false description: The PVC provisioner profile for the VM - in: query name: migration_method type: string required: false description: The preferred migration method (live, shutdown, none) default: none enum: - live - shutdown - none responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.update_vm_meta( vm, reqargs.get('limit', None), reqargs.get('selector', None), reqargs.get('autostart', None), reqargs.get('profile', None), reqargs.get('migration_method', None) ) api.add_resource(API_VM_Metadata, '/vm//meta') # /vm//state') # /vm//node class API_VM_Node(Resource): @Authenticator def get(self, vm): """ Return the node information of {vm} --- tags: - vm responses: 200: description: OK schema: type: object id: VMNode properties: name: type: string description: The name of the VM node: type: string description: The node the VM is currently assigned to last_node: type: string description: The last node the VM was assigned to before migrating 404: description: Not found schema: type: object id: Message """ return api_helper.vm_node(vm) @RequestParser([ {'name': 'action', 'choices': ('migrate', 'unmigrate', 'move'), 'helptext': "A valid action must be specified", 'required': True}, {'name': 'node'}, {'name': 'force'}, {'name': 'wait'}, {'name': 'force_live'} ]) @Authenticator def post(self, vm, reqargs): """ Set the node of {vm} --- tags: - vm parameters: - in: query name: action type: string required: true description: The action to take to change nodes enum: - migrate - unmigrate - move - in: query name: node type: string description: The node the VM should be assigned to; autoselect if empty or invalid - in: query name: force type: boolean description: Whether to force an already-migrated VM to a new node - in: query name: wait type: boolean description: Whether to block waiting for the migration to complete - in: query name: force_live type: boolean description: Whether to enforce live migration and disable shutdown-based fallback migration responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ action = reqargs.get('action', None) node = reqargs.get('node', None) force = bool(strtobool(reqargs.get('force', 'false'))) wait = bool(strtobool(reqargs.get('wait', 'false'))) force_live = bool(strtobool(reqargs.get('force_live', 'false'))) if action == 'move': return api_helper.vm_move(vm, node, wait, force_live) if action == 'migrate': return api_helper.vm_migrate(vm, node, force, wait, force_live) if action == 'unmigrate': return api_helper.vm_unmigrate(vm, wait, force_live) abort(400) api.add_resource(API_VM_Node, '/vm//node') # /vm//locks class API_VM_Locks(Resource): @Authenticator def post(self, vm): """ Flush disk locks of {vm} --- tags: - vm responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.vm_flush_locks(vm) api.add_resource(API_VM_Locks, '/vm//locks') # /vm//console') # /vm//rename class API_VM_Rename(Resource): @RequestParser([ {'name': 'new_name'} ]) @Authenticator def post(self, vm, reqargs): """ Rename VM {vm}, and all connected disk volumes which include this name, to {new_name} --- tags: - vm parameters: - in: query name: new_name type: string required: true description: The new name of the VM responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.vm_rename( vm, reqargs.get('new_name', None) ) api.add_resource(API_VM_Rename, '/vm//rename') ########################################################## # Client API - Network ########################################################## # /network class API_Network_Root(Resource): @RequestParser([ {'name': 'limit'} ]) @Authenticator def get(self, reqargs): """ Return a list of networks in the cluster --- tags: - network definitions: - schema: type: object id: network properties: vni: type: integer description: The VNI of the network description: type: string description: The description of the network type: type: string description: The type of network enum: - managed - bridged domain: type: string description: The DNS domain of the network ("managed" networks only) name_servers: type: array description: The configured DNS nameservers of the network for NS records ("managed" networks only) items: type: string ip4: type: object description: The IPv4 details of the network ("managed" networks only) properties: network: type: string description: The IPv4 network subnet in CIDR format gateway: type: string description: The IPv4 default gateway address dhcp_flag: type: boolean description: Whether DHCP is enabled dhcp_start: type: string description: The IPv4 DHCP pool start address dhcp_end: type: string description: The IPv4 DHCP pool end address ip6: type: object description: The IPv6 details of the network ("managed" networks only) properties: network: type: string description: The IPv6 network subnet in CIDR format gateway: type: string description: The IPv6 default gateway address dhcp_flag: type: boolean description: Whether DHCPv6 is enabled parameters: - in: query name: limit type: string required: false description: A VNI or description search limit; fuzzy by default, use ^/$ to force exact matches responses: 200: description: OK schema: type: array items: $ref: '#/definitions/network' """ return api_helper.net_list(reqargs.get('limit', None)) @RequestParser([ {'name': 'vni', 'required': True}, {'name': 'description', 'required': True}, {'name': 'nettype', 'choices': ('managed', 'bridged'), 'helptext': 'A valid nettype must be specified', 'required': True}, {'name': 'domain'}, {'name': 'name_servers'}, {'name': 'ip4_network'}, {'name': 'ip4_gateway'}, {'name': 'ip6_network'}, {'name': 'ip6_gateway'}, {'name': 'dhcp4'}, {'name': 'dhcp4_start'}, {'name': 'dhcp4_end'} ]) @Authenticator def post(self, reqargs): """ Create a new network --- tags: - network parameters: - in: query name: vni type: integer required: true description: The VNI of the network - in: query name: description type: string required: true description: The description of the network - in: query name: nettype type: string required: true description: The type of network enum: - managed - bridged - in: query name: domain type: string description: The DNS domain of the network ("managed" networks only) - in: query name: name_servers type: string description: The CSV list of DNS nameservers for network NS records ("managed" networks only) - in: query name: ip4_network type: string description: The IPv4 network subnet of the network in CIDR format; IPv4 disabled if unspecified ("managed" networks only) - in: query name: ip4_gateway type: string description: The IPv4 default gateway address of the network ("managed" networks only) - in: query name: dhcp4 type: boolean description: Whether to enable DHCPv4 for the network ("managed" networks only) - in: query name: dhcp4_start type: string description: The DHCPv4 pool start address of the network ("managed" networks only) - in: query name: dhcp4_end type: string description: The DHCPv4 pool end address of the network ("managed" networks only) - in: query name: ip6_network type: string description: The IPv6 network subnet of the network in CIDR format; IPv6 disabled if unspecified; DHCPv6 is always used in IPv6 managed networks ("managed" networks only) - in: query name: ip6_gateway type: string description: The IPv6 default gateway address of the network ("managed" networks only) responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ if reqargs.get('name_servers', None): name_servers = ','.join(reqargs.get('name_servers', None)) else: name_servers = '' return api_helper.net_add( reqargs.get('vni', None), reqargs.get('description', None), reqargs.get('nettype', None), reqargs.get('domain', None), name_servers, reqargs.get('ip4_network', None), reqargs.get('ip4_gateway', None), reqargs.get('ip6_network', None), reqargs.get('ip6_gateway', None), bool(strtobool(reqargs.get('dhcp4', 'false'))), reqargs.get('dhcp4_start', None), reqargs.get('dhcp4_end', None), ) api.add_resource(API_Network_Root, '/network') # /network/ class API_Network_Element(Resource): @Authenticator def get(self, vni): """ Return information about network {vni} --- tags: - network responses: 200: description: OK schema: $ref: '#/definitions/network' 404: description: Not found schema: type: object id: Message """ return api_helper.net_list(vni, is_fuzzy=False) @RequestParser([ {'name': 'description', 'required': True}, {'name': 'nettype', 'choices': ('managed', 'bridged'), 'helptext': 'A valid nettype must be specified', 'required': True}, {'name': 'domain'}, {'name': 'name_servers'}, {'name': 'ip4_network'}, {'name': 'ip4_gateway'}, {'name': 'ip6_network'}, {'name': 'ip6_gateway'}, {'name': 'dhcp4'}, {'name': 'dhcp4_start'}, {'name': 'dhcp4_end'} ]) @Authenticator def post(self, vni, reqargs): """ Create a new network {vni} --- tags: - network parameters: - in: query name: description type: string required: true description: The description of the network - in: query name: nettype type: string required: true description: The type of network enum: - managed - bridged - in: query name: domain type: string description: The DNS domain of the network ("managed" networks only) - in: query name: name_servers type: string description: The CSV list of DNS nameservers for network NS records ("managed" networks only) - in: query name: ip4_network type: string description: The IPv4 network subnet of the network in CIDR format; IPv4 disabled if unspecified ("managed" networks only) - in: query name: ip4_gateway type: string description: The IPv4 default gateway address of the network ("managed" networks only) - in: query name: dhcp4 type: boolean description: Whether to enable DHCPv4 for the network ("managed" networks only) - in: query name: dhcp4_start type: string description: The DHCPv4 pool start address of the network ("managed" networks only) - in: query name: dhcp4_end type: string description: The DHCPv4 pool end address of the network ("managed" networks only) - in: query name: ip6_network type: string description: The IPv6 network subnet of the network in CIDR format; IPv6 disabled if unspecified; DHCPv6 is always used in IPv6 managed networks ("managed" networks only) - in: query name: ip6_gateway type: string description: The IPv6 default gateway address of the network ("managed" networks only) responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ if reqargs.get('name_servers', None): name_servers = ','.join(reqargs.get('name_servers', None)) else: name_servers = '' return api_helper.net_add( reqargs.get('vni', None), reqargs.get('description', None), reqargs.get('nettype', None), reqargs.get('domain', None), name_servers, reqargs.get('ip4_network', None), reqargs.get('ip4_gateway', None), reqargs.get('ip6_network', None), reqargs.get('ip6_gateway', None), bool(strtobool(reqargs.get('dhcp4', 'false'))), reqargs.get('dhcp4_start', None), reqargs.get('dhcp4_end', None), ) @RequestParser([ {'name': 'description'}, {'name': 'domain'}, {'name': 'name_servers'}, {'name': 'ip4_network'}, {'name': 'ip4_gateway'}, {'name': 'ip6_network'}, {'name': 'ip6_gateway'}, {'name': 'dhcp4'}, {'name': 'dhcp4_start'}, {'name': 'dhcp4_end'} ]) @Authenticator def put(self, vni, reqargs): """ Update details of network {vni} Note: A network's type cannot be changed; the network must be removed and recreated as the new type --- tags: - network parameters: - in: query name: description type: string description: The description of the network - in: query name: domain type: string description: The DNS domain of the network ("managed" networks only) - in: query name: name_servers type: string description: The CSV list of DNS nameservers for network NS records ("managed" networks only) - in: query name: ip4_network type: string description: The IPv4 network subnet of the network in CIDR format; IPv4 disabled if unspecified ("managed" networks only) - in: query name: ip4_gateway type: string description: The IPv4 default gateway address of the network ("managed" networks only) - in: query name: dhcp4 type: boolean description: Whether to enable DHCPv4 for the network ("managed" networks only) - in: query name: dhcp4_start type: string description: The DHCPv4 pool start address of the network ("managed" networks only) - in: query name: dhcp4_end type: string description: The DHCPv4 pool end address of the network ("managed" networks only) - in: query name: ip6_network type: string description: The IPv6 network subnet of the network in CIDR format; IPv6 disabled if unspecified; DHCPv6 is always used in IPv6 managed networks ("managed" networks only) - in: query name: ip6_gateway type: string description: The IPv6 default gateway address of the network ("managed" networks only) responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ if reqargs.get('name_servers', None): name_servers = ','.join(reqargs.get('name_servers', None)) else: name_servers = '' return api_helper.net_modify( vni, reqargs.get('description', None), reqargs.get('domain', None), name_servers, reqargs.get('ip4_network', None), reqargs.get('ip4_gateway', None), reqargs.get('ip6_network', None), reqargs.get('ip6_gateway', None), reqargs.get('dhcp4', None), reqargs.get('dhcp4_start', None), reqargs.get('dhcp4_end', None), ) @Authenticator def delete(self, vni): """ Remove network {vni} --- tags: - network responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ return api_helper.net_remove(vni) api.add_resource(API_Network_Element, '/network/') # /network//lease class API_Network_Lease_Root(Resource): @RequestParser([ {'name': 'limit'}, {'name': 'static'} ]) @Authenticator def get(self, vni, reqargs): """ Return a list of DHCP leases in network {vni} --- tags: - network definitions: - schema: type: object id: lease properties: hostname: type: string description: The (short) hostname of the lease ip4_address: type: string description: The IPv4 address of the lease mac_address: type: string description: The MAC address of the lease timestamp: type: integer description: The UNIX timestamp of the lease creation parameters: - in: query name: limit type: string required: false description: A MAC address search limit; fuzzy by default, use ^/$ to force exact matches - in: query name: static type: boolean required: false default: false description: Whether to show only static leases responses: 200: description: OK schema: type: array items: $ref: '#/definitions/lease' 400: description: Bad request schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ return api_helper.net_dhcp_list( vni, reqargs.get('limit', None), bool(strtobool(reqargs.get('static', 'false'))) ) @RequestParser([ {'name': 'macaddress', 'required': True}, {'name': 'ipaddress', 'required': True}, {'name': 'hostname'} ]) @Authenticator def post(self, vni, reqargs): """ Create a new static DHCP lease in network {vni} --- tags: - network parameters: - in: query name: macaddress type: string required: false description: A MAC address for the lease - in: query name: ipaddress type: string required: false description: An IPv4 address for the lease - in: query name: hostname type: string required: false description: An optional hostname for the lease responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ return api_helper.net_dhcp_add( vni, reqargs.get('ipaddress', None), reqargs.get('macaddress', None), reqargs.get('hostname', None) ) api.add_resource(API_Network_Lease_Root, '/network//lease') # /network//lease/{mac} class API_Network_Lease_Element(Resource): @Authenticator def get(self, vni, mac): """ Return information about DHCP lease {mac} in network {vni} --- tags: - network definitions: - schema: type: object id: lease properties: hostname: type: string description: The (short) hostname of the lease ip4_address: type: string description: The IPv4 address of the lease mac_address: type: string description: The MAC address of the lease timestamp: type: integer description: The UNIX timestamp of the lease creation responses: 200: description: OK schema: type: array items: $ref: '#/definitions/lease' 400: description: Bad request schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ return api_helper.net_dhcp_list( vni, mac, False ) @RequestParser([ {'name': 'ipaddress', 'required': True}, {'name': 'hostname'} ]) @Authenticator def post(self, vni, mac, reqargs): """ Create a new static DHCP lease {mac} in network {vni} --- tags: - network parameters: - in: query name: macaddress type: string required: false description: A MAC address for the lease - in: query name: ipaddress type: string required: false description: An IPv4 address for the lease - in: query name: hostname type: string required: false description: An optional hostname for the lease responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ return api_helper.net_dhcp_add( vni, reqargs.get('ipaddress', None), mac, reqargs.get('hostname', None) ) @Authenticator def delete(self, vni, mac): """ Delete static DHCP lease {mac} --- tags: - network responses: 200: description: OK schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ return api_helper.net_dhcp_remove( vni, mac ) api.add_resource(API_Network_Lease_Element, '/network//lease/') # /network//acl class API_Network_ACL_Root(Resource): @RequestParser([ {'name': 'limit'}, {'name': 'direction', 'choices': ('in', 'out'), 'helptext': "A valid direction must be specified."} ]) @Authenticator def get(self, vni, reqargs): """ Return a list of ACLs in network {vni} --- tags: - network definitions: - schema: type: object id: acl properties: description: type: string description: The description of the rule direction: type: string description: The direction the rule applies in order: type: integer description: The order of the rule in the chain rule: type: string description: The NFT-format rule string parameters: - in: query name: limit type: string required: false description: A description search limit; fuzzy by default, use ^/$ to force exact matches - in: query name: direction type: string required: false description: The direction of rules to display; both directions shown if unspecified responses: 200: description: OK schema: type: array items: $ref: '#/definitions/acl' 400: description: Bad request schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ return api_helper.net_acl_list( vni, reqargs.get('limit', None), reqargs.get('direction', None) ) @RequestParser([ {'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 def post(self, vni, reqargs): """ Create a new ACL in network {vni} --- tags: - network parameters: - in: query name: description type: string required: true description: A whitespace-free description/name for the ACL - in: query name: direction type: string required: false description: The direction of the ACL; defaults to "in" if unspecified enum: - in - out - in: query name: order type: integer description: The order of the ACL in the chain; defaults to the end - in: query name: rule type: string required: true description: The raw NFT firewall rule string responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.net_acl_add( vni, reqargs.get('direction', 'in'), reqargs.get('description', None), reqargs.get('rule', None), reqargs.get('order', None) ) api.add_resource(API_Network_ACL_Root, '/network//acl') # /network//acl/ class API_Network_ACL_Element(Resource): @Authenticator def get(self, vni, description): """ Return information about ACL {description} in network {vni} --- tags: - network responses: 200: description: OK schema: $ref: '#/definitions/acl' 400: description: Bad request schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ return api_helper.net_acl_list( vni, description, None, is_fuzzy=False ) @RequestParser([ {'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 def post(self, vni, description, reqargs): """ Create a new ACL {description} in network {vni} --- tags: - network parameters: - in: query name: direction type: string required: false description: The direction of the ACL; defaults to "in" if unspecified enum: - in - out - in: query name: order type: integer description: The order of the ACL in the chain; defaults to the end - in: query name: rule type: string required: true description: The raw NFT firewall rule string responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.net_acl_add( vni, reqargs.get('direction', 'in'), description, reqargs.get('rule', None), reqargs.get('order', None) ) @Authenticator def delete(self, vni, description): """ Delete ACL {description} in network {vni} --- tags: - network responses: 200: description: OK schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ return api_helper.net_acl_remove( vni, description ) api.add_resource(API_Network_ACL_Element, '/network//acl/') ########################################################## # Client API - Storage ########################################################## # Note: The prefix `/storage` allows future potential storage subsystems. # Since Ceph is the only section not abstracted by PVC directly # (i.e. it references Ceph-specific concepts), this makes more # sense in the long-term.# # /storage class API_Storage_Root(Resource): @Authenticator def get(self): pass api.add_resource(API_Storage_Root, '/storage') # /storage/ceph class API_Storage_Ceph_Root(Resource): @Authenticator def get(self): pass api.add_resource(API_Storage_Ceph_Root, '/storage/ceph') # /storage/ceph/status class API_Storage_Ceph_Status(Resource): @Authenticator def get(self): """ Return status data for the PVC Ceph cluster --- tags: - storage / ceph responses: 200: description: OK schema: type: object properties: type: type: string description: The type of Ceph data returned primary_node: type: string description: The curent primary node in the cluster ceph_data: type: string description: The raw output data """ return api_helper.ceph_status() api.add_resource(API_Storage_Ceph_Status, '/storage/ceph/status') # /storage/ceph/utilization class API_Storage_Ceph_Utilization(Resource): @Authenticator def get(self): """ Return utilization data for the PVC Ceph cluster --- tags: - storage / ceph responses: 200: description: OK schema: type: object properties: type: type: string description: The type of Ceph data returned primary_node: type: string description: The curent primary node in the cluster ceph_data: type: string description: The raw output data """ return api_helper.ceph_util() api.add_resource(API_Storage_Ceph_Utilization, '/storage/ceph/utilization') # /storage/ceph/benchmark class API_Storage_Ceph_Benchmark(Resource): @RequestParser([ {'name': 'job'} ]) @Authenticator def get(self, reqargs): """ List results from benchmark jobs --- tags: - storage / ceph parameters: - in: query name: job type: string required: false description: A single job name to limit results to responses: 200: description: OK schema: type: object id: storagebenchmark properties: id: type: string (containing integer) description: The database ID of the test result job: type: string description: The job name (an ISO date) of the test result benchmark_result: type: object properties: test_name: type: object properties: overall: type: object properties: iosize: type: string (integer) description: The total size of the benchmark data bandwidth: type: string (integer) description: The average bandwidth (KiB/s) iops: type: string (integer) description: The average IOPS runtime: type: string (integer) description: The total test time in milliseconds latency: type: object properties: min: type: string (integer) description: The minimum latency measurement max: type: string (integer) description: The maximum latency measurement mean: type: string (float) description: The mean latency measurement stdev: type: string (float) description: The standard deviation of latency bandwidth: type: object properties: min: type: string (integer) description: The minimum bandwidth (KiB/s) measurement max: type: string (integer) description: The maximum bandwidth (KiB/s) measurement mean: type: string (float) description: The mean bandwidth (KiB/s) measurement stdev: type: string (float) description: The standard deviation of bandwidth numsamples: type: string (integer) description: The number of samples taken during the test iops: type: object properties: min: type: string (integer) description: The minimum IOPS measurement max: type: string (integer) description: The maximum IOPS measurement mean: type: string (float) description: The mean IOPS measurement stdev: type: string (float) description: The standard deviation of IOPS numsamples: type: string (integer) description: The number of samples taken during the test cpu: type: object properties: user: type: string (float percentage) description: The percentage of test time spent in user space system: type: string (float percentage) description: The percentage of test time spent in system (kernel) space ctxsw: type: string (integer) description: The number of context switches during the test majfault: type: string (integer) description: The number of major page faults during the test minfault: type: string (integer) description: The number of minor page faults during the test """ return api_benchmark.list_benchmarks(reqargs.get('job', None)) @RequestParser([ {'name': 'pool', 'required': True, 'helptext': "A valid pool must be specified."}, ]) @Authenticator def post(self, reqargs): """ Execute a storage benchmark against a storage pool --- tags: - storage / ceph parameters: - in: query name: pool type: string required: true description: The PVC storage pool to benchmark responses: 200: description: OK schema: type: string description: The Celery job ID of the benchmark (unused elsewhere) """ # Verify that the pool is valid _list, code = api_helper.ceph_pool_list(reqargs.get('pool', None), is_fuzzy=False) if code != 200: return {'message': 'Pool "{}" is not valid.'.format(reqargs.get('pool'))}, 400 task = run_benchmark.delay( reqargs.get('pool', None) ) return {"task_id": task.id}, 202, {'Location': Api.url_for(api, API_Storage_Ceph_Benchmark, task_id=task.id)} api.add_resource(API_Storage_Ceph_Benchmark, '/storage/ceph/benchmark') # /storage/ceph/option class API_Storage_Ceph_Option(Resource): @RequestParser([ {'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): """ Set or unset OSD options on the Ceph cluster --- tags: - storage / ceph parameters: - in: query name: option type: string required: true description: The Ceph OSD option to act on; must be valid to "ceph osd set/unset" - in: query name: action type: string required: true description: The action to take enum: - set - unset responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ if reqargs.get('action') == 'set': return api_helper.ceph_osd_set(reqargs.get('option')) if reqargs.get('action') == 'unset': return api_helper.ceph_osd_unset(reqargs.get('option')) abort(400) api.add_resource(API_Storage_Ceph_Option, '/storage/ceph/option') # /storage/ceph/osd class API_Storage_Ceph_OSD_Root(Resource): @RequestParser([ {'name': 'limit'}, ]) @Authenticator def get(self, reqargs): """ Return a list of Ceph OSDs in the cluster --- tags: - storage / ceph definitions: - schema: type: object id: osd properties: id: type: string (containing integer) description: The Ceph ID of the OSD stats: type: object properties: uuid: type: string description: The Ceph OSD UUID up: type: boolean integer description: Whether OSD is in "up" state in: type: boolean integer description: Whether OSD is in "in" state primary_affinity: type: integer description: The Ceph primary affinity of the OSD utilization: type: number description: The utilization percentage of the OSD var: type: number description: The usage variability among OSDs pgs: type: integer description: The number of placement groups on this OSD kb: type: integer description: Size of the OSD in KB weight: type: number description: The weight of the OSD in the CRUSH map reweight: type: number description: The active cluster weight of the OSD node: type: string description: The PVC node the OSD resides on used: type: string description: The used space on the OSD in human-readable format avail: type: string description: The free space on the OSD in human-readable format wr_ops: type: integer description: Cluster-lifetime write operations to OSD rd_ops: type: integer description: Cluster-lifetime read operations from OSD wr_data: type: integer description: Cluster-lifetime write size to OSD rd_data: type: integer description: Cluster-lifetime read size from OSD state: type: string description: CSV of the current state of the OSD parameters: - in: query name: limit type: string required: false description: A OSD ID search limit; fuzzy by default, use ^/$ to force exact matches responses: 200: description: OK schema: type: array items: $ref: '#/definitions/osd' """ return api_helper.ceph_osd_list( reqargs.get('limit', None) ) @RequestParser([ {'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): """ Add a Ceph OSD to the cluster Note: This task may take up to 30s to complete and return --- tags: - storage / ceph parameters: - in: query name: node type: string required: true description: The PVC node to create the OSD on - in: query name: device type: string required: true description: The block device (e.g. "/dev/sdb", "/dev/disk/by-path/...", etc.) to create the OSD on - in: query name: weight type: number required: true description: The Ceph CRUSH weight for the OSD responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.ceph_osd_add( reqargs.get('node', None), reqargs.get('device', None), reqargs.get('weight', None) ) api.add_resource(API_Storage_Ceph_OSD_Root, '/storage/ceph/osd') # /storage/ceph/osd/ class API_Storage_Ceph_OSD_Element(Resource): @Authenticator def get(self, osdid): """ Return information about Ceph OSD {osdid} --- tags: - storage / ceph responses: 200: description: OK schema: $ref: '#/definitions/osd' """ return api_helper.ceph_osd_list( osdid ) @RequestParser([ {'name': 'yes-i-really-mean-it', 'required': True, 'helptext': "Please confirm that 'yes-i-really-mean-it'."} ]) @Authenticator def delete(self, osdid, reqargs): """ Remove Ceph OSD {osdid} Note: This task may take up to 30s to complete and return Warning: This operation may have unintended consequences for the storage cluster; ensure the cluster can support removing the OSD before proceeding --- tags: - storage / ceph 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 schema: type: object id: Message 404: description: Not found schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.ceph_osd_remove( osdid ) api.add_resource(API_Storage_Ceph_OSD_Element, '/storage/ceph/osd/') # /storage/ceph/osd//state class API_Storage_Ceph_OSD_State(Resource): @Authenticator def get(self, osdid): """ Return the current state of OSD {osdid} --- tags: - storage / ceph responses: 200: description: OK schema: type: object properties: state: type: string description: The current OSD state """ return api_helper.ceph_osd_state( osdid ) @RequestParser([ {'name': 'state', 'choices': ('in', 'out'), 'required': True, 'helptext': "A valid state must be specified."}, ]) @Authenticator def post(self, osdid, reqargs): """ Set the current state of OSD {osdid} --- tags: - storage / ceph parameters: - in: query name: state type: string required: true description: Set the OSD to this state responses: 200: description: OK schema: type: object id: Message """ if reqargs.get('state', None) == 'in': return api_helper.ceph_osd_in( osdid ) if reqargs.get('state', None) == 'out': return api_helper.ceph_osd_out( osdid ) abort(400) api.add_resource(API_Storage_Ceph_OSD_State, '/storage/ceph/osd//state') # /storage/ceph/pool class API_Storage_Ceph_Pool_Root(Resource): @RequestParser([ {'name': 'limit'} ]) @Authenticator def get(self, reqargs): """ Return a list of pools in the cluster --- tags: - storage / ceph definitions: - schema: type: object id: pool properties: name: type: string description: The name of the pool volume_count: type: integer description: The number of volumes in the pool stats: type: object properties: id: type: integer description: The Ceph pool ID stored_bytes: type: integer description: The stored data size (in bytes, post-replicas) free_bytes: type: integer description: The total free space (in bytes. post-replicas) used_bytes: type: integer description: The total used space (in bytes, pre-replicas) used_percent: type: number description: The ratio of used space to free space num_objects: type: integer description: The number of Ceph objects before replication num_object_clones: type: integer description: The total number of cloned Ceph objects num_object_copies: type: integer description: The total number of Ceph objects after replication num_objects_missing_on_primary: type: integer description: The total number of missing-on-primary Ceph objects num_objects_unfound: type: integer description: The total number of unfound Ceph objects num_objects_degraded: type: integer description: The total number of degraded Ceph objects read_ops: type: integer description: The total read operations on the pool (pool-lifetime) read_bytes: type: integer description: The total read bytes on the pool (pool-lifetime) write_ops: type: integer description: The total write operations on the pool (pool-lifetime) write_bytes: type: integer description: The total write bytes on the pool (pool-lifetime) parameters: - in: query name: limit type: string required: false description: A pool name search limit; fuzzy by default, use ^/$ to force exact matches responses: 200: description: OK schema: type: array items: $ref: '#/definitions/pool' """ return api_helper.ceph_pool_list( reqargs.get('limit', None) ) @RequestParser([ {'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): """ Create a new Ceph pool --- tags: - storage / ceph parameters: - in: query name: pool type: string required: true description: The name of the pool - in: query name: pgs type: integer required: true description: The number of placement groups (PGs) for the pool - in: query name: replcfg type: string required: true description: The replication configuration (e.g. "copies=3,mincopies=2") for the pool responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.ceph_pool_add( reqargs.get('pool', None), reqargs.get('pgs', None), reqargs.get('replcfg', None) ) api.add_resource(API_Storage_Ceph_Pool_Root, '/storage/ceph/pool') # /storage/ceph/pool/ class API_Storage_Ceph_Pool_Element(Resource): @Authenticator def get(self, pool): """ Return information about {pool} --- tags: - storage / ceph responses: 200: description: OK schema: $ref: '#/definitions/pool' 404: description: Not found schema: type: object id: Message """ return api_helper.ceph_pool_list( pool, is_fuzzy=False ) @RequestParser([ {'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): """ Create a new Ceph pool {pool} --- tags: - storage / ceph parameters: - in: query name: pgs type: integer required: true description: The number of placement groups (PGs) for the pool - in: query name: replcfg type: string required: true description: The replication configuration (e.g. "copies=3,mincopies=2") for the pool 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 """ return api_helper.ceph_pool_add( pool, reqargs.get('pgs', None), reqargs.get('replcfg', None) ) @RequestParser([ {'name': 'yes-i-really-mean-it', 'required': True, 'helptext': "Please confirm that 'yes-i-really-mean-it'."} ]) @Authenticator def delete(self, pool, reqargs): """ Remove Ceph pool {pool} Note: This task may take up to 30s to complete and return --- tags: - storage / ceph 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 schema: type: object id: Message 404: description: Not found schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.ceph_pool_remove( pool ) api.add_resource(API_Storage_Ceph_Pool_Element, '/storage/ceph/pool/') # /storage/ceph/volume class API_Storage_Ceph_Volume_Root(Resource): @RequestParser([ {'name': 'limit'}, {'name': 'pool'} ]) @Authenticator def get(self, reqargs): """ Return a list of volumes in the cluster --- tags: - storage / ceph definitions: - schema: type: object id: volume properties: name: type: string description: The name of the volume pool: type: string description: The name of the pool containing the volume stats: type: object properties: name: type: string description: The name of the volume id: type: string description: The Ceph volume ID size: type: string description: The size of the volume (human-readable values) objects: type: integer description: The number of Ceph objects making up the volume order: type: integer description: The Ceph volume order ID object_size: type: integer description: The size of each object in bytes snapshot_count: type: integer description: The number of snapshots of the volume block_name_prefix: type: string description: The Ceph-internal block name prefix format: type: integer description: The Ceph RBD volume format features: type: array items: type: string description: The Ceph RBD feature op_features: type: array items: type: string description: The Ceph RBD operational features flags: type: array items: type: string description: The Ceph RBD volume flags create_timestamp: type: string description: The volume creation timestamp access_timestamp: type: string description: The volume access timestamp modify_timestamp: type: string description: The volume modification timestamp parameters: - in: query name: limit type: string required: false description: A volume name search limit; fuzzy by default, use ^/$ to force exact matches - in: query name: pool type: string required: false description: A pool to limit the search to responses: 200: description: OK schema: type: array items: $ref: '#/definitions/volume' """ return api_helper.ceph_volume_list( reqargs.get('pool', None), reqargs.get('limit', None) ) @RequestParser([ {'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): """ Create a new Ceph volume --- tags: - storage / ceph parameters: - in: query name: volume type: string required: true description: The name of the volume - in: query name: pool type: integer required: true description: The name of the pool to contain the volume - in: query name: size type: string required: true description: The volume size in bytes (or with a metric suffix, i.e. k/M/G/T) responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.ceph_volume_add( reqargs.get('pool', None), reqargs.get('volume', None), reqargs.get('size', None) ) api.add_resource(API_Storage_Ceph_Volume_Root, '/storage/ceph/volume') # /storage/ceph/volume// class API_Storage_Ceph_Volume_Element(Resource): @Authenticator def get(self, pool, volume): """ Return information about volume {volume} in pool {pool} --- tags: - storage / ceph responses: 200: description: OK schema: $ref: '#/definitions/volume' 404: description: Not found schema: type: object id: Message """ return api_helper.ceph_volume_list( pool, volume, is_fuzzy=False ) @RequestParser([ {'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): """ Create a new Ceph volume {volume} in pool {pool} --- tags: - storage / ceph parameters: - in: query name: size type: string required: true description: The volume size in bytes (or with a metric suffix, i.e. k/M/G/T) 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 """ return api_helper.ceph_volume_add( pool, volume, reqargs.get('size', None) ) @RequestParser([ {'name': 'new_size'}, {'name': 'new_name'} ]) @Authenticator def put(self, pool, volume, reqargs): """ Update the size or name of Ceph volume {volume} in pool {pool} --- tags: - storage / ceph parameters: - in: query name: size type: string required: false description: The new volume size in bytes (or with a metric suffix, i.e. k/M/G/T); must be greater than the previous size (shrinking not supported) - in: query name: new_name type: string required: false description: The new volume name 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 """ if reqargs.get('new_size', None) and reqargs.get('new_name', None): return {"message": "Can only perform one modification at once"}, 400 if reqargs.get('new_size', None): return api_helper.ceph_volume_resize( pool, volume, reqargs.get('new_size') ) if reqargs.get('new_name', None): return api_helper.ceph_volume_rename( pool, volume, reqargs.get('new_name') ) return {"message": "At least one modification must be specified"}, 400 @Authenticator def delete(self, pool, volume): """ Remove Ceph volume {volume} from pool {pool} Note: This task may take up to 30s to complete and return depending on the size of the volume --- tags: - storage / ceph responses: 200: description: OK schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ return api_helper.ceph_volume_remove( pool, volume ) api.add_resource(API_Storage_Ceph_Volume_Element, '/storage/ceph/volume//') # /storage/ceph/volume///clone class API_Storage_Ceph_Volume_Element_Clone(Resource): @RequestParser([ {'name': 'new_volume', 'required': True, 'helptext': "A new volume name must be specified."} ]) @Authenticator def post(self, pool, volume, reqargs): """ Clone Ceph volume {volume} in pool {pool} --- tags: - storage / ceph parameters: - in: query name: new_volume type: string required: true description: The name of the new cloned volume 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 """ return api_helper.ceph_volume_clone( pool, reqargs.get('new_volume', None), volume ) 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): @RequestParser([ {'name': 'image_format', 'required': True, 'location': ['args'], 'helptext': "A source image format must be specified."} ]) @Authenticator def post(self, pool, volume, reqargs): """ Upload a disk image to Ceph volume {volume} in pool {pool} The body must be a form body containing a file that is the binary contents of the image. --- tags: - storage / ceph parameters: - in: query name: image_format type: string required: true description: The type of source image file enum: - raw - vmdk - qcow2 - qed - vdi - vpc 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 """ return api_helper.ceph_volume_upload( pool, volume, reqargs.get('image_format', None) ) api.add_resource(API_Storage_Ceph_Volume_Element_Upload, '/storage/ceph/volume///upload') # /storage/ceph/snapshot class API_Storage_Ceph_Snapshot_Root(Resource): @RequestParser([ {'name': 'pool'}, {'name': 'volume'}, {'name': 'limit'}, ]) @Authenticator def get(self, reqargs): """ Return a list of snapshots in the cluster --- tags: - storage / ceph definitions: - schema: type: object id: snapshot properties: snapshot: type: string description: The name of the snapshot volume: type: string description: The name of the volume pool: type: string description: The name of the pool parameters: - in: query name: limit type: string required: false description: A volume name search limit; fuzzy by default, use ^/$ to force exact matches - in: query name: pool type: string required: false description: A pool to limit the search to - in: query name: volume type: string required: false description: A volume to limit the search to responses: 200: description: OK schema: type: array items: $ref: '#/definitions/snapshot' """ return api_helper.ceph_volume_snapshot_list( reqargs.get('pool', None), reqargs.get('volume', None), reqargs.get('limit', None) ) @RequestParser([ {'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): """ Create a new Ceph snapshot --- tags: - storage / ceph parameters: - in: query name: snapshot type: string required: true description: The name of the snapshot - in: query name: volume type: string required: true description: The name of the volume - in: query name: pool type: integer required: true description: The name of the pool responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ return api_helper.ceph_volume_snapshot_add( reqargs.get('pool', None), reqargs.get('volume', None), reqargs.get('snapshot', None) ) api.add_resource(API_Storage_Ceph_Snapshot_Root, '/storage/ceph/snapshot') # /storage/ceph/snapshot/// class API_Storage_Ceph_Snapshot_Element(Resource): @Authenticator def get(self, pool, volume, snapshot): """ Return information about snapshot {snapshot} of volume {volume} in pool {pool} --- tags: - storage / ceph responses: 200: description: OK schema: $ref: '#/definitions/snapshot' 404: description: Not found schema: type: object id: Message """ return api_helper.ceph_volume_snapshot_list( pool, volume, snapshot, is_fuzzy=False ) @Authenticator def post(self, pool, volume, snapshot): """ Create a new Ceph snapshot {snapshot} of volume {volume} in pool {pool} --- tags: - storage / ceph parameters: - in: query name: snapshot type: string required: true description: The name of the snapshot - in: query name: volume type: string required: true description: The name of the volume - in: query name: pool type: integer required: true description: The name of the pool 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 """ return api_helper.ceph_volume_snapshot_add( pool, volume, snapshot ) @RequestParser([ {'name': 'new_name', 'required': True, 'helptext': "A new name must be specified."} ]) @Authenticator def put(self, pool, volume, snapshot, reqargs): """ Update the name of Ceph snapshot {snapshot} of volume {volume} in pool {pool} --- tags: - storage / ceph parameters: - in: query name: new_name type: string required: false description: The new snaoshot name 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 """ return api_helper.ceph_volume_snapshot_rename( pool, volume, snapshot, reqargs.get('new_name', None) ) @Authenticator def delete(self, pool, volume, snapshot): """ Remove Ceph snapshot {snapshot} of volume {volume} from pool {pool} Note: This task may take up to 30s to complete and return depending on the size of the snapshot --- tags: - storage / ceph responses: 200: description: OK schema: type: object id: Message 404: description: Not found schema: type: object id: Message """ return api_helper.ceph_volume_snapshot_remove( pool, volume, snapshot ) api.add_resource(API_Storage_Ceph_Snapshot_Element, '/storage/ceph/snapshot///') ########################################################## # Provisioner API ########################################################## # /provisioner class API_Provisioner_Root(Resource): @Authenticator def get(self): """ Unused endpoint """ abort(404) api.add_resource(API_Provisioner_Root, '/provisioner') # /provisioner/template class API_Provisioner_Template_Root(Resource): @RequestParser([ {'name': 'limit'} ]) @Authenticator def get(self, reqargs): """ Return a list of all templates --- tags: - provisioner / template definitions: - schema: type: object id: all-templates properties: system-templates: type: array items: $ref: '#/definitions/system-template' network-templates: type: array items: $ref: '#/definitions/network-template' storage-templates: type: array items: $ref: '#/definitions/storage-template' parameters: - in: query name: limit type: string required: false description: A template name search limit; fuzzy by default, use ^/$ to force exact matches responses: 200: description: OK schema: $ref: '#/definitions/all-templates' """ return api_provisioner.template_list( reqargs.get('limit', None) ) api.add_resource(API_Provisioner_Template_Root, '/provisioner/template') # /provisioner/template/system class API_Provisioner_Template_System_Root(Resource): @RequestParser([ {'name': 'limit'} ]) @Authenticator def get(self, reqargs): """ Return a list of system templates --- tags: - provisioner / template definitions: - schema: type: object id: system-template properties: id: type: integer description: Internal provisioner template ID name: type: string description: Template name vcpu_count: type: integer description: vCPU count for VM vram_mb: type: integer description: vRAM size in MB for VM serial: type: boolean description: Whether to enable serial console for VM vnc: type: boolean description: Whether to enable VNC console for VM vnc_bind: type: string description: VNC bind address when VNC console is enabled node_limit: type: string description: CSV list of node(s) to limit VM assignment to node_selector: type: string description: Selector to use for VM node assignment on migration/move node_autostart: type: boolean description: Whether to start VM with node ready state (one-time) migration_method: type: string description: The preferred migration method (live, shutdown, none) parameters: - in: query name: limit type: string required: false description: A template name search limit; fuzzy by default, use ^/$ to force exact matches responses: 200: description: OK schema: type: list items: $ref: '#/definitions/system-template' """ return api_provisioner.list_template_system( reqargs.get('limit', None) ) @RequestParser([ {'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'}, {'name': 'node_autostart'}, {'name': 'migration_method'} ]) @Authenticator def post(self, reqargs): """ Create a new system template --- tags: - provisioner / template parameters: - in: query name: name type: string required: true description: Template name - in: query name: vcpus type: integer required: true description: vCPU count for VM - in: query name: vram type: integer required: true description: vRAM size in MB for VM - in: query name: serial type: boolean required: true description: Whether to enable serial console for VM - in: query name: vnc type: boolean required: true description: Whether to enable VNC console for VM - in: query name: vnc_bind type: string required: false description: VNC bind address when VNC console is enabled - in: query name: node_limit type: string required: false description: CSV list of node(s) to limit VM assignment to - in: query name: node_selector type: string required: false description: Selector to use for VM node assignment on migration/move - in: query name: node_autostart type: boolean required: false description: Whether to start VM with node ready state (one-time) - in: query name: migration_method type: string required: false description: The preferred migration method (live, shutdown, none) responses: 200: description: OK schema: type: object id: Message 400: description: Bad request schema: type: object id: Message """ # Validate arguments try: vcpus = int(reqargs.get('vcpus')) except Exception: return {"message": "A vcpus value must be an integer"}, 400 try: vram = int(reqargs.get('vram')) except Exception: return {"message": "A vram value must be an integer"}, 400 # Cast boolean arguments if bool(strtobool(reqargs.get('serial', 'false'))): serial = True else: serial = False if bool(strtobool(reqargs.get('vnc', 'false'))): vnc = True vnc_bind = reqargs.get('vnc_bind', None) else: vnc = False vnc_bind = None if reqargs.get('node_autostart', None) and bool(strtobool(reqargs.get('node_autostart', 'false'))): node_autostart = True else: node_autostart = False return api_provisioner.create_template_system( reqargs.get('name'), vcpus, vram, serial, vnc, vnc_bind, reqargs.get('node_limit', None), reqargs.get('node_selector', None), node_autostart, reqargs.get('migration_method', None), ) api.add_resource(API_Provisioner_Template_System_Root, '/provisioner/template/system') # /provisioner/template/system/