10094 lines
300 KiB
Python
Executable File
10094 lines
300 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Daemon.py - PVC HTTP API daemon
|
|
# Part of the Parallel Virtual Cluster (PVC) system
|
|
#
|
|
# Copyright (C) 2018-2024 Joshua M. Boniface <joshua@boniface.me>
|
|
#
|
|
# 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 <https://www.gnu.org/licenses/>.
|
|
#
|
|
###############################################################################
|
|
|
|
import flask
|
|
|
|
from functools import wraps
|
|
from flask_restful import Resource, Api, reqparse, abort
|
|
from celery import Celery
|
|
from kombu import Queue
|
|
from lxml.objectify import fromstring as lxml_fromstring
|
|
from uuid import uuid4
|
|
|
|
from daemon_lib.common import getPrimaryNode
|
|
from daemon_lib.zkhandler import ZKConnection
|
|
from daemon_lib.node import get_list as get_node_list
|
|
from daemon_lib.benchmark import list_benchmarks
|
|
|
|
from pvcapid.Daemon import config, strtobool, API_VERSION
|
|
|
|
import pvcapid.helper as api_helper
|
|
import pvcapid.provisioner as api_provisioner
|
|
import pvcapid.ova as api_ova
|
|
|
|
from flask_sqlalchemy import SQLAlchemy
|
|
|
|
|
|
# Create Flask app and set config values
|
|
app = flask.Flask(__name__)
|
|
|
|
# Set up SQLAlchemy backend
|
|
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True
|
|
app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://{}:{}@{}:{}/{}".format(
|
|
config["api_postgresql_user"],
|
|
config["api_postgresql_password"],
|
|
config["api_postgresql_host"],
|
|
config["api_postgresql_port"],
|
|
config["api_postgresql_dbname"],
|
|
)
|
|
|
|
if config["debug"]:
|
|
app.config["DEBUG"] = True
|
|
else:
|
|
app.config["DEBUG"] = False
|
|
|
|
if config["api_auth_enabled"]:
|
|
app.config["SECRET_KEY"] = config["api_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)
|
|
|
|
|
|
# Set up Celery queues
|
|
@ZKConnection(config)
|
|
def get_all_nodes(zkhandler):
|
|
_, all_nodes = get_node_list(zkhandler, None)
|
|
return [n["name"] for n in all_nodes]
|
|
|
|
|
|
@ZKConnection(config)
|
|
def get_primary_node(zkhandler):
|
|
return getPrimaryNode(zkhandler)
|
|
|
|
|
|
# Set up Celery task ID generator
|
|
# 1. Lets us make our own IDs (first section of UUID)
|
|
# 2. Lets us distribute jobs to the required pvcworkerd instances
|
|
def run_celery_task(task_name, **kwargs):
|
|
task_id = str(uuid4()).split("-")[0]
|
|
|
|
if "run_on" in kwargs and kwargs["run_on"] != "primary":
|
|
run_on = kwargs["run_on"]
|
|
else:
|
|
run_on = get_primary_node()
|
|
|
|
print(
|
|
f"Incoming pvcworkerd task: '{task_name}' ({task_id}) assigned to worker {run_on} with args {kwargs}"
|
|
)
|
|
|
|
task = celery.send_task(
|
|
task_name,
|
|
task_id=task_id,
|
|
kwargs=kwargs,
|
|
queue=run_on,
|
|
)
|
|
|
|
return task
|
|
|
|
|
|
# Create celery definition
|
|
celery_task_uri = "redis://{}:{}{}".format(
|
|
config["keydb_host"], config["keydb_port"], config["keydb_path"]
|
|
)
|
|
celery = Celery(
|
|
app.name,
|
|
broker=celery_task_uri,
|
|
backend=celery_task_uri,
|
|
result_extended=True,
|
|
)
|
|
app.config["broker_url"] = celery_task_uri
|
|
app.config["result_backend"] = celery_task_uri
|
|
celery.conf.update(app.config)
|
|
|
|
|
|
def celery_startup():
|
|
"""
|
|
Runs when the API daemon starts, but not the Celery workers or the API doc generator
|
|
"""
|
|
app.config["task_queues"] = tuple(
|
|
[Queue(h, routing_key=f"{h}.#") for h in get_all_nodes()]
|
|
)
|
|
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["api_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["api_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
|
|
|
|
|
|
##########################################################
|
|
# 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["api_auth_enabled"]:
|
|
return flask.redirect(Api.url_for(api, API_Root))
|
|
|
|
if any(
|
|
token
|
|
for token in config["api_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["api_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 `/config/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") == "True":
|
|
overwrite_flag = True
|
|
else:
|
|
overwrite_flag = False
|
|
|
|
return api_helper.initialize_cluster(overwrite=overwrite_flag)
|
|
|
|
|
|
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:
|
|
cluster_health:
|
|
type: object
|
|
properties:
|
|
health:
|
|
type: integer
|
|
description: The overall health (%) of the cluster
|
|
example: 100
|
|
messages:
|
|
type: array
|
|
description: A list of health event strings
|
|
items:
|
|
type: string
|
|
example: "hv1: plugin 'nics': bond0 DEGRADED with 1 active slaves, bond0 OK at 10000 Mbps"
|
|
node_health:
|
|
type: object
|
|
properties:
|
|
hvX:
|
|
type: object
|
|
description: A node entry for per-node health details, one per node in the cluster
|
|
properties:
|
|
health:
|
|
type: integer
|
|
description: The health (%) of the node
|
|
example: 100
|
|
messages:
|
|
type: array
|
|
description: A list of health event strings
|
|
items:
|
|
type: string
|
|
example: "'nics': bond0 DEGRADED with 1 active slaves, bond0 OK at 10000 Mbps"
|
|
maintenance:
|
|
type: string
|
|
description: Whether the cluster is in maintenance mode or not (string boolean)
|
|
example: true
|
|
primary_node:
|
|
type: string
|
|
description: The current primary coordinator node
|
|
example: pvchv1
|
|
pvc_version:
|
|
type: string
|
|
description: The PVC version of the current primary coordinator node
|
|
example: 0.9.61
|
|
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
|
|
resources:
|
|
type: object
|
|
properties:
|
|
memory:
|
|
type: object
|
|
properties:
|
|
total:
|
|
type: integer
|
|
description: The total amount of RAM (all nodes) in MB
|
|
used:
|
|
type: integer
|
|
description: The total used RAM (all nodes) in MB
|
|
free:
|
|
type: integer
|
|
description: The total free RAM (all nodes) 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) in MB
|
|
utilization:
|
|
type: float
|
|
description: The memory utilization percentage (average) of the cluster
|
|
vcpu:
|
|
type: object
|
|
properties:
|
|
total:
|
|
type: integer
|
|
description: The total number of real CPU cores (all nodes)
|
|
load:
|
|
type: float
|
|
description: The current 5-minute CPU load (all nodes summed)
|
|
allocated:
|
|
type: integer
|
|
description: The total number of vCPUs allocated to running domains
|
|
provisioned:
|
|
type: integer
|
|
description: The total number of vCPUs provisioned to all domains (regardless of state)
|
|
utilization:
|
|
type: float
|
|
description: The CPU utilization percentage (average) of the cluster
|
|
disk:
|
|
type: object
|
|
properties:
|
|
total:
|
|
type: integer
|
|
description: The total size of all OSDs in KB
|
|
used:
|
|
type: integer
|
|
description: The total used size of all OSDs in KB
|
|
free:
|
|
type: integer
|
|
description: The total free size of all OSDs in KB
|
|
utilization:
|
|
type: float
|
|
description: The disk utilization percentage (average) of the 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:
|
|
- root
|
|
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")
|
|
|
|
|
|
# /status/primary_node
|
|
class API_Status_Primary(Resource):
|
|
def get(self):
|
|
"""
|
|
Return the name of the current primary node.
|
|
---
|
|
tags:
|
|
- root
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
properties:
|
|
primary_node:
|
|
type: string
|
|
description: The name of the current primary node
|
|
204:
|
|
description: No content
|
|
schema:
|
|
type: object
|
|
properties:
|
|
primary_node:
|
|
type: string
|
|
description: An empty response; there is not currently a primary node, try again later
|
|
"""
|
|
primary_node = get_primary_node()
|
|
if primary_node is None:
|
|
retdata = None
|
|
retcode = 204
|
|
else:
|
|
retdata = {"primary_node": primary_node}
|
|
retcode = 200
|
|
return retdata, retcode
|
|
|
|
|
|
api.add_resource(API_Status_Primary, "/status/primary_node")
|
|
|
|
|
|
# /metrics
|
|
class API_Metrics(Resource):
|
|
def get(self):
|
|
"""
|
|
Return the current PVC cluster status in Prometheus-compatible metrics format and
|
|
the Ceph cluster metrics as one document.
|
|
|
|
Endpoint is UNAUTHENTICATED to allow metrics exfiltration without having to deal
|
|
with Prometheus compatibility (only basic auth support). Ensure this API endpoint
|
|
is only opened to trusted networks that cannot abuse the data provided!
|
|
---
|
|
tags:
|
|
- root
|
|
responses:
|
|
200:
|
|
description: OK
|
|
400:
|
|
description: Bad request
|
|
"""
|
|
health_output, health_retcode = api_helper.cluster_health_metrics()
|
|
resource_output, resource_retcode = api_helper.cluster_resource_metrics()
|
|
ceph_output, ceph_retcode = api_helper.ceph_metrics()
|
|
zookeeper_output, zookeeper_retcode = api_helper.zookeeper_metrics()
|
|
|
|
if health_retcode != 200 or resource_retcode != 200 or ceph_retcode != 200:
|
|
output = "Error: Failed to obtain data"
|
|
retcode = 400
|
|
else:
|
|
output = health_output + resource_output + ceph_output + zookeeper_output
|
|
retcode = 200
|
|
|
|
response = flask.make_response(output, retcode)
|
|
response.mimetype = "text/plain"
|
|
return response
|
|
|
|
|
|
if config["enable_prometheus"]:
|
|
api.add_resource(API_Metrics, "/metrics")
|
|
|
|
|
|
# /metrics/health
|
|
class API_Metrics_Health(Resource):
|
|
def get(self):
|
|
"""
|
|
Return the current PVC cluster health status in Prometheus-compatible metrics format
|
|
|
|
Endpoint is UNAUTHENTICATED to allow metrics exfiltration without having to deal
|
|
with Prometheus compatibility (only basic auth support). Ensure this API endpoint
|
|
is only opened to trusted networks that cannot abuse the data provided!
|
|
---
|
|
tags:
|
|
- root
|
|
responses:
|
|
200:
|
|
description: OK
|
|
400:
|
|
description: Bad request
|
|
"""
|
|
health_output, health_retcode = api_helper.cluster_health_metrics()
|
|
|
|
if health_retcode != 200:
|
|
output = "Error: Failed to obtain data"
|
|
retcode = 400
|
|
else:
|
|
output = health_output
|
|
retcode = 200
|
|
|
|
response = flask.make_response(output, retcode)
|
|
response.mimetype = "text/plain"
|
|
return response
|
|
|
|
|
|
if config["enable_prometheus"]:
|
|
api.add_resource(API_Metrics_Health, "/metrics/health")
|
|
|
|
|
|
# /metrics/resource
|
|
class API_Metrics_Resource(Resource):
|
|
def get(self):
|
|
"""
|
|
Return the current PVC cluster resource utilizations in Prometheus-compatible metrics format
|
|
|
|
Endpoint is UNAUTHENTICATED to allow metrics exfiltration without having to deal
|
|
with Prometheus compatibility (only basic auth support). Ensure this API endpoint
|
|
is only opened to trusted networks that cannot abuse the data provided!
|
|
---
|
|
tags:
|
|
- root
|
|
responses:
|
|
200:
|
|
description: OK
|
|
400:
|
|
description: Bad request
|
|
"""
|
|
resource_output, resource_retcode = api_helper.cluster_resource_metrics()
|
|
|
|
if resource_retcode != 200:
|
|
output = "Error: Failed to obtain data"
|
|
retcode = 400
|
|
else:
|
|
output = resource_output
|
|
retcode = 200
|
|
|
|
response = flask.make_response(output, retcode)
|
|
response.mimetype = "text/plain"
|
|
return response
|
|
|
|
|
|
if config["enable_prometheus"]:
|
|
api.add_resource(API_Metrics_Resource, "/metrics/resource")
|
|
|
|
|
|
# /metrics/ceph
|
|
class API_Metrics_Ceph(Resource):
|
|
def get(self):
|
|
"""
|
|
Return the current PVC Ceph Prometheus metrics
|
|
|
|
Proxies a metrics request to the current active MGR, since this is dynamic
|
|
and can't be controlled by PVC easily.
|
|
|
|
Endpoint is UNAUTHENTICATED to allow metrics exfiltration without having to deal
|
|
with Prometheus compatibility (only basic auth support). Ensure this API endpoint
|
|
is only opened to trusted networks that cannot abuse the data provided!
|
|
---
|
|
tags:
|
|
- root
|
|
responses:
|
|
200:
|
|
description: OK
|
|
400:
|
|
description: Bad request
|
|
"""
|
|
ceph_output, ceph_retcode = api_helper.ceph_metrics()
|
|
|
|
if ceph_retcode != 200:
|
|
output = "Error: Failed to obtain data"
|
|
retcode = 400
|
|
else:
|
|
output = ceph_output
|
|
retcode = 200
|
|
|
|
response = flask.make_response(output, retcode)
|
|
response.mimetype = "text/plain"
|
|
return response
|
|
|
|
|
|
if config["enable_prometheus"]:
|
|
api.add_resource(API_Metrics_Ceph, "/metrics/ceph")
|
|
|
|
|
|
# /metrics/zookeeper
|
|
class API_Metrics_Zookeeper(Resource):
|
|
def get(self):
|
|
"""
|
|
Return the current PVC Zookeeper Prometheus metrics
|
|
|
|
Proxies a metrics request to the current primary node, since all coordinators
|
|
run an active Zookeeper instance and we want one central location.
|
|
|
|
Endpoint is UNAUTHENTICATED to allow metrics exfiltration without having to deal
|
|
with Prometheus compatibility (only basic auth support). Ensure this API endpoint
|
|
is only opened to trusted networks that cannot abuse the data provided!
|
|
---
|
|
tags:
|
|
- root
|
|
responses:
|
|
200:
|
|
description: OK
|
|
400:
|
|
description: Bad request
|
|
"""
|
|
zookeeper_output, zookeeper_retcode = api_helper.zookeeper_metrics()
|
|
|
|
if zookeeper_retcode != 200:
|
|
output = "Error: Failed to obtain data"
|
|
retcode = 400
|
|
else:
|
|
output = zookeeper_output
|
|
retcode = 200
|
|
|
|
response = flask.make_response(output, retcode)
|
|
response.mimetype = "text/plain"
|
|
return response
|
|
|
|
|
|
if config["enable_prometheus"]:
|
|
api.add_resource(API_Metrics_Zookeeper, "/metrics/zookeeper")
|
|
|
|
|
|
# /faults
|
|
class API_Faults(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "sort_key",
|
|
"choices": (
|
|
"first_reported",
|
|
"last_reported",
|
|
"acknowledged_at",
|
|
"status",
|
|
"health_delta",
|
|
"message",
|
|
),
|
|
"helptext": "A valid sort key must be specified",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def get(self, reqargs):
|
|
"""
|
|
Return a list of cluster faults
|
|
---
|
|
tags:
|
|
- faults
|
|
definitions:
|
|
- schema:
|
|
type: object
|
|
id: fault
|
|
properties:
|
|
id:
|
|
type: string
|
|
description: The ID of the fault
|
|
example: "10ae144b78b4cc5fdf09e2ebbac51235"
|
|
first_reported:
|
|
type: date
|
|
description: The first time the fault was reported
|
|
example: "2023-12-01 16:47:59.849742"
|
|
last_reported:
|
|
type: date
|
|
description: The last time the fault was reported
|
|
example: "2023-12-01 17:39:45.188398"
|
|
acknowledged_at:
|
|
type: date
|
|
description: The time the fault was acknowledged, or empty if not acknowledged
|
|
example: "2023-12-01 17:50:00.000000"
|
|
status:
|
|
type: string
|
|
description: The current state of the fault, either "new" or "ack" (acknowledged)
|
|
example: "new"
|
|
health_delta:
|
|
type: integer
|
|
description: The health delta (amount it reduces cluster health from 100%) of the fault
|
|
example: 25
|
|
message:
|
|
type: string
|
|
description: The textual description of the fault
|
|
example: "Node hv1 was at 40% (psur@-10%, psql@-50%) <= 50% health"
|
|
parameters:
|
|
- in: query
|
|
name: sort_key
|
|
type: string
|
|
required: false
|
|
description: The fault object key to sort results by
|
|
enum:
|
|
- first_reported
|
|
- last_reported
|
|
- acknowledged_at
|
|
- status
|
|
- health_delta
|
|
- message
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/definitions/fault'
|
|
"""
|
|
return api_helper.fault_list(sort_key=reqargs.get("sort_key", "last_reported"))
|
|
|
|
@Authenticator
|
|
def put(self):
|
|
"""
|
|
Acknowledge all cluster faults
|
|
---
|
|
tags:
|
|
- faults
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
properties:
|
|
message:
|
|
type: string
|
|
description: A text message
|
|
"""
|
|
return api_helper.fault_acknowledge_all()
|
|
|
|
@Authenticator
|
|
def delete(self):
|
|
"""
|
|
Delete all cluster faults
|
|
---
|
|
tags:
|
|
- faults
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
properties:
|
|
message:
|
|
type: string
|
|
description: A text message
|
|
"""
|
|
return api_helper.fault_delete_all()
|
|
|
|
|
|
api.add_resource(API_Faults, "/faults")
|
|
|
|
|
|
# /faults/<fault_id>
|
|
class API_Faults_Element(Resource):
|
|
@Authenticator
|
|
def get(self, fault_id):
|
|
"""
|
|
Return a single cluster fault
|
|
---
|
|
tags:
|
|
- faults
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/definitions/fault'
|
|
"""
|
|
return api_helper.fault_list(limit=fault_id)
|
|
|
|
@Authenticator
|
|
def put(self, fault_id):
|
|
"""
|
|
Acknowledge a cluster fault
|
|
---
|
|
tags:
|
|
- faults
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
properties:
|
|
message:
|
|
type: string
|
|
description: A text message
|
|
"""
|
|
return api_helper.fault_acknowledge(fault_id)
|
|
|
|
@Authenticator
|
|
def delete(self, fault_id):
|
|
"""
|
|
Delete a cluster fault
|
|
---
|
|
tags:
|
|
- faults
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
properties:
|
|
message:
|
|
type: string
|
|
description: A text message
|
|
"""
|
|
return api_helper.fault_delete(fault_id)
|
|
|
|
|
|
api.add_resource(API_Faults_Element, "/faults/<fault_id>")
|
|
|
|
|
|
# /tasks
|
|
class API_Tasks(Resource):
|
|
@Authenticator
|
|
def get(self):
|
|
"""
|
|
Return a list of active Celery worker tasks
|
|
---
|
|
tags:
|
|
- root
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
properties:
|
|
active:
|
|
type: object
|
|
description: Celery app.control.inspect active tasks
|
|
reserved:
|
|
type: object
|
|
description: Celery app.control.inspect reserved tasks
|
|
scheduled:
|
|
type: object
|
|
description: Celery app.control.inspect scheduled tasks
|
|
"""
|
|
queue = celery.control.inspect()
|
|
response = {
|
|
"scheduled": queue.scheduled(),
|
|
"active": queue.active(),
|
|
"reserved": queue.reserved(),
|
|
}
|
|
return response
|
|
|
|
|
|
api.add_resource(API_Tasks, "/tasks")
|
|
|
|
|
|
# /tasks/<task_id>
|
|
class API_Tasks_Element(Resource):
|
|
@Authenticator
|
|
def get(self, task_id):
|
|
"""
|
|
View status of a Celery worker task {task_id}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
properties:
|
|
total:
|
|
type: integer
|
|
description: Total number of steps
|
|
current:
|
|
type: integer
|
|
description: Current steps completed
|
|
state:
|
|
type: string
|
|
description: Current job state
|
|
status:
|
|
type: string
|
|
description: Status details about job
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
task = celery.AsyncResult(task_id)
|
|
if task.state == "PENDING":
|
|
response = {
|
|
"state": task.state,
|
|
"current": 0,
|
|
"total": 1,
|
|
"status": "Pending job start",
|
|
}
|
|
elif task.state == "FAILURE":
|
|
response = {
|
|
"state": task.state,
|
|
"current": 1,
|
|
"total": 1,
|
|
"status": str(task.info),
|
|
}
|
|
else:
|
|
response = {
|
|
"state": task.state,
|
|
"current": task.info.get("current", 0),
|
|
"total": task.info.get("total", 1),
|
|
"status": task.info.get("status", ""),
|
|
}
|
|
if "result" in task.info:
|
|
response["result"] = task.info["result"]
|
|
return response
|
|
|
|
|
|
api.add_resource(API_Tasks_Element, "/tasks/<task_id>")
|
|
|
|
|
|
##########################################################
|
|
# 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
|
|
pvc_version:
|
|
type: string
|
|
description: The current running PVC node daemon version
|
|
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
|
|
health:
|
|
type: integer
|
|
description: The overall health (%) of the node
|
|
example: 100
|
|
health_plugins:
|
|
type: array
|
|
description: A list of health plugin names currently loaded on the node
|
|
items:
|
|
type: string
|
|
example: "nics"
|
|
health_details:
|
|
type: array
|
|
description: A list of health plugin results
|
|
items:
|
|
type: object
|
|
properties:
|
|
name:
|
|
type: string
|
|
description: The name of the health plugin
|
|
example: nics
|
|
last_run:
|
|
type: integer
|
|
description: The UNIX timestamp (s) of the last plugin run
|
|
example: 1676786078
|
|
health_delta:
|
|
type: integer
|
|
description: The health delta (negatively applied to the health percentage) of the plugin's current state
|
|
example: 10
|
|
message:
|
|
type: string
|
|
description: The output message of the plugin
|
|
example: "bond0 DEGRADED with 1 active slaves, bond0 OK at 10000 Mbps"
|
|
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
|
|
interfaces:
|
|
type: object
|
|
description: Details on speed, bytes, and packets per second of each node physical network interface
|
|
parameters:
|
|
- in: query
|
|
name: limit
|
|
type: string
|
|
required: false
|
|
description: A search limit in the name, tags, or an exact UUID; 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/<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>")
|
|
|
|
|
|
# /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/<node>/daemon-state")
|
|
|
|
|
|
# /node/<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/<node>/coordinator-state")
|
|
|
|
|
|
# /node/<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/<node>/domain-state")
|
|
|
|
|
|
# /node/<node</log
|
|
class API_Node_Log(Resource):
|
|
@RequestParser([{"name": "lines"}])
|
|
@Authenticator
|
|
def get(self, node, reqargs):
|
|
"""
|
|
Return the recent logs of {node}
|
|
---
|
|
tags:
|
|
- node
|
|
parameters:
|
|
- in: query
|
|
name: lines
|
|
type: integer
|
|
required: false
|
|
description: The number of lines to retrieve
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: NodeLog
|
|
properties:
|
|
name:
|
|
type: string
|
|
description: The name of the Node
|
|
data:
|
|
type: string
|
|
description: The recent log text
|
|
404:
|
|
description: Node not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_helper.node_log(node, reqargs.get("lines", None))
|
|
|
|
|
|
api.add_resource(API_Node_Log, "/node/<node>/log")
|
|
|
|
|
|
##########################################################
|
|
# Client API - VM
|
|
##########################################################
|
|
|
|
|
|
# /vm
|
|
class API_VM_Root(Resource):
|
|
@RequestParser(
|
|
[
|
|
{"name": "limit"},
|
|
{"name": "node"},
|
|
{"name": "state"},
|
|
{"name": "tag"},
|
|
{"name": "negate"},
|
|
]
|
|
)
|
|
@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 <last_node>"
|
|
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; see 'target_selector' in the node daemon configuration reference
|
|
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)
|
|
migration_max_downtime:
|
|
type: integer
|
|
description: The maximum time in milliseconds that a VM can be down for during a live migration; busy VMs may require a larger max_downtime
|
|
tags:
|
|
type: array
|
|
description: The tag(s) of the VM
|
|
items:
|
|
type: object
|
|
id: VMTag
|
|
properties:
|
|
name:
|
|
type: string
|
|
description: The name of the tag
|
|
type:
|
|
type: string
|
|
description: The type of the tag (user, system)
|
|
protected:
|
|
type: boolean
|
|
description: Whether the tag is protected or not
|
|
snapshots:
|
|
type: array
|
|
description: The snapshot(s) of the VM
|
|
items:
|
|
type: object
|
|
id: VMSnapshot
|
|
properties:
|
|
name:
|
|
type: string
|
|
description: The name of the snapshot
|
|
timestamp:
|
|
type: string
|
|
descrpition: Unix timestamp of the snapshot
|
|
age:
|
|
type: string
|
|
description: Human-readable age of the snapshot in the largest viable unit (seconds, minutes, hours, days)
|
|
rbd_snapshots:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: A list of RBD volume snapshots belonging to this VM snapshot, in '<pool>/<volume>@<snapshot>' format
|
|
xml_diff_lines:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: A list of strings representing the lines of an (n=1) unified diff between the current VM XML specification and the snapshot VM XML specification
|
|
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 search limit in the name, tags, or an exact UUID; 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
|
|
- in: query
|
|
name: tag
|
|
type: string
|
|
required: false
|
|
description: Limit list to VMs with this tag
|
|
- in: query
|
|
name: negate
|
|
type: boolean
|
|
required: false
|
|
description: Negate the specified node, state, or tag limit(s)
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/definitions/vm'
|
|
"""
|
|
return api_helper.vm_list(
|
|
node=reqargs.get("node", None),
|
|
state=reqargs.get("state", None),
|
|
tag=reqargs.get("tag", None),
|
|
limit=reqargs.get("limit", None),
|
|
negate=bool(strtobool(reqargs.get("negate", "False"))),
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{"name": "limit"},
|
|
{"name": "node"},
|
|
{
|
|
"name": "selector",
|
|
"choices": ("mem", "memprov", "vcpus", "load", "vms", "none"),
|
|
"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": "migration_max_downtime",
|
|
"helptext": "A valid migration_max_downtime must be specified",
|
|
},
|
|
{"name": "user_tags", "action": "append"},
|
|
{"name": "protected_tags", "action": "append"},
|
|
{
|
|
"name": "xml",
|
|
"required": True,
|
|
"helptext": "A Libvirt XML document must be specified",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, reqargs):
|
|
"""
|
|
Define/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; see 'target_selector' in the node daemon configuration reference
|
|
default: none
|
|
enum:
|
|
- mem
|
|
- memprov
|
|
- vcpus
|
|
- load
|
|
- vms
|
|
- none (cluster default)
|
|
- 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
|
|
- in: query
|
|
name: migration_max_downtime
|
|
type: integer
|
|
required: false
|
|
description: The maximum time in milliseconds that a VM can be down for during a live migration; busy VMs may require a larger max_downtime
|
|
default: 300
|
|
- in: query
|
|
name: user_tags
|
|
type: array
|
|
required: false
|
|
description: The user tag(s) of the VM
|
|
items:
|
|
type: string
|
|
- in: query
|
|
name: protected_tags
|
|
type: array
|
|
required: false
|
|
description: The protected user tag(s) of the VM
|
|
items:
|
|
type: string
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
user_tags = reqargs.get("user_tags", None)
|
|
if user_tags is None:
|
|
user_tags = []
|
|
protected_tags = reqargs.get("protected_tags", None)
|
|
if protected_tags is None:
|
|
protected_tags = []
|
|
|
|
return api_helper.vm_define(
|
|
reqargs.get("xml"),
|
|
reqargs.get("node", None),
|
|
reqargs.get("limit", None),
|
|
reqargs.get("selector", "none"),
|
|
bool(strtobool(reqargs.get("autostart", "false"))),
|
|
reqargs.get("migration_method", "none"),
|
|
reqargs.get("migration_max_downtime", 300),
|
|
user_tags,
|
|
protected_tags,
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Root, "/vm")
|
|
|
|
|
|
# /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(
|
|
node=None, state=None, tag=None, limit=vm, is_fuzzy=False, negate=False
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{"name": "limit"},
|
|
{"name": "node"},
|
|
{
|
|
"name": "selector",
|
|
"choices": ("mem", "memprov", "vcpus", "load", "vms", "none"),
|
|
"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": "migration_max_downtime",
|
|
"helptext": "A valid migration_max_downtime must be specified",
|
|
},
|
|
{"name": "user_tags", "action": "append"},
|
|
{"name": "protected_tags", "action": "append"},
|
|
{
|
|
"name": "xml",
|
|
"required": True,
|
|
"helptext": "A Libvirt XML document must be specified",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Define/create a new virtual machine
|
|
|
|
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; see 'target_selector' in the node daemon configuration reference
|
|
default: none
|
|
enum:
|
|
- mem
|
|
- memprov
|
|
- vcpus
|
|
- load
|
|
- vms
|
|
- none (cluster default)
|
|
- 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
|
|
- in: query
|
|
name: migration_max_downtime
|
|
type: integer
|
|
required: false
|
|
description: The maximum time in milliseconds that a VM can be down for during a live migration; busy VMs may require a larger max_downtime
|
|
default: 300
|
|
- in: query
|
|
name: user_tags
|
|
type: array
|
|
required: false
|
|
description: The user tag(s) of the VM
|
|
items:
|
|
type: string
|
|
- in: query
|
|
name: protected_tags
|
|
type: array
|
|
required: false
|
|
description: The protected user tag(s) of the VM
|
|
items:
|
|
type: string
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
user_tags = reqargs.get("user_tags", None)
|
|
if user_tags is None:
|
|
user_tags = []
|
|
protected_tags = reqargs.get("protected_tags", None)
|
|
if protected_tags is None:
|
|
protected_tags = []
|
|
|
|
return api_helper.vm_define(
|
|
reqargs.get("xml"),
|
|
reqargs.get("node", None),
|
|
reqargs.get("limit", None),
|
|
reqargs.get("selector", "none"),
|
|
bool(strtobool(reqargs.get("autostart", "false"))),
|
|
reqargs.get("migration_method", "none"),
|
|
reqargs.get("migration_max_downtime", 300),
|
|
user_tags,
|
|
protected_tags,
|
|
)
|
|
|
|
@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>")
|
|
|
|
|
|
# /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; see 'target_selector' in the node daemon configuration reference
|
|
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)
|
|
migration_max_downtime:
|
|
type: integer
|
|
description: The maximum time in milliseconds that a VM can be down for during a live migration; busy VMs may require a larger max_downtime
|
|
404:
|
|
description: VM not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_helper.get_vm_meta(vm)
|
|
|
|
@RequestParser(
|
|
[
|
|
{"name": "limit"},
|
|
{
|
|
"name": "selector",
|
|
"choices": ("mem", "memprov", "vcpus", "load", "vms", "none"),
|
|
"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",
|
|
},
|
|
{
|
|
"name": "migration_max_downtime",
|
|
"helptext": "A valid migration_max_downtime 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; see 'target_selector' in the node daemon configuration reference
|
|
enum:
|
|
- mem
|
|
- memprov
|
|
- vcpus
|
|
- load
|
|
- vms
|
|
- none (cluster default)
|
|
- 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
|
|
- in: query
|
|
name: migration_max_downtime
|
|
type: integer
|
|
required: false
|
|
description: The maximum time in milliseconds that a VM can be down for during a live migration; busy VMs may require a larger max_downtime
|
|
default: none
|
|
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
|
|
"""
|
|
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),
|
|
reqargs.get("migration_max_downtime", None),
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Metadata, "/vm/<vm>/meta")
|
|
|
|
|
|
# /vm/<vm>/tags
|
|
class API_VM_Tags(Resource):
|
|
@Authenticator
|
|
def get(self, vm):
|
|
"""
|
|
Return the tags of {vm}
|
|
---
|
|
tags:
|
|
- vm
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: VMTags
|
|
properties:
|
|
name:
|
|
type: string
|
|
description: The name of the VM
|
|
tags:
|
|
type: array
|
|
description: The tag(s) of the VM
|
|
items:
|
|
type: object
|
|
id: VMTag
|
|
404:
|
|
description: VM not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_helper.get_vm_tags(vm)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "action",
|
|
"choices": ("add", "remove"),
|
|
"helptext": "A valid action must be specified",
|
|
},
|
|
{"name": "tag"},
|
|
{"name": "protected"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Set the tags of {vm}
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: action
|
|
type: string
|
|
required: true
|
|
description: The action to perform with the tag
|
|
enum:
|
|
- add
|
|
- remove
|
|
- in: query
|
|
name: tag
|
|
type: string
|
|
required: true
|
|
description: The text value of the tag
|
|
- in: query
|
|
name: protected
|
|
type: boolean
|
|
required: false
|
|
default: false
|
|
description: Set the protected state of the tag
|
|
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
|
|
"""
|
|
return api_helper.update_vm_tag(
|
|
vm,
|
|
reqargs.get("action"),
|
|
reqargs.get("tag"),
|
|
reqargs.get("protected", False),
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Tags, "/vm/<vm>/tags")
|
|
|
|
|
|
# /vm/<vm</state
|
|
class API_VM_State(Resource):
|
|
@Authenticator
|
|
def get(self, vm):
|
|
"""
|
|
Return the state information of {vm}
|
|
---
|
|
tags:
|
|
- vm
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: VMState
|
|
properties:
|
|
name:
|
|
type: string
|
|
description: The name of the VM
|
|
state:
|
|
type: string
|
|
description: The current state of the VM
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_helper.vm_state(vm)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "state",
|
|
"choices": ("start", "shutdown", "stop", "restart", "disable"),
|
|
"helptext": "A valid state must be specified",
|
|
"required": True,
|
|
},
|
|
{"name": "force"},
|
|
{"name": "wait"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Set the state of {vm}
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: state
|
|
type: string
|
|
required: true
|
|
description: The new state of the VM
|
|
enum:
|
|
- start
|
|
- shutdown
|
|
- stop
|
|
- restart
|
|
- disable
|
|
- in: query
|
|
name: force
|
|
type: boolean
|
|
description: For "disable", force stop instead of shutdown and/or force mirror state; for "start" or "stop", force mirror state.
|
|
- in: query
|
|
name: wait
|
|
type: boolean
|
|
description: Whether to block waiting for the state change to complete
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
state = reqargs.get("state", None)
|
|
force = bool(strtobool(reqargs.get("force", "false")))
|
|
wait = bool(strtobool(reqargs.get("wait", "false")))
|
|
|
|
if state == "start":
|
|
return api_helper.vm_start(vm, force=force)
|
|
if state == "shutdown":
|
|
return api_helper.vm_shutdown(vm, wait)
|
|
if state == "stop":
|
|
return api_helper.vm_stop(vm, force=force)
|
|
if state == "restart":
|
|
return api_helper.vm_restart(vm, wait)
|
|
if state == "disable":
|
|
return api_helper.vm_disable(vm, force=force)
|
|
abort(400)
|
|
|
|
|
|
api.add_resource(API_VM_State, "/vm/<vm>/state")
|
|
|
|
|
|
# /vm/<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/<vm>/node")
|
|
|
|
|
|
# /vm/<vm>/locks
|
|
class API_VM_Locks(Resource):
|
|
@Authenticator
|
|
def post(self, vm):
|
|
"""
|
|
Flush disk locks of {vm}
|
|
---
|
|
tags:
|
|
- vm
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
properties:
|
|
task_id:
|
|
type: string
|
|
description: The internal Celery job ID of the task
|
|
task_name:
|
|
type: string
|
|
description: The internal Celery job task name
|
|
run_on:
|
|
type: string
|
|
description: The worker daemon instance assigned to run the task
|
|
|
|
"""
|
|
vm_node_detail, retcode = api_helper.vm_node(vm)
|
|
if retcode == 200:
|
|
vm_node = vm_node_detail["node"]
|
|
else:
|
|
return vm_node_detail, retcode
|
|
|
|
task = run_celery_task("vm.flush_locks", domain=vm, run_on=vm_node)
|
|
|
|
return (
|
|
{"task_id": task.id, "task_name": "vm.flush_locks", "run_on": vm_node},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Locks, "/vm/<vm>/locks")
|
|
|
|
|
|
# /vm/<vm>/console
|
|
class API_VM_Console(Resource):
|
|
@RequestParser([{"name": "lines"}])
|
|
@Authenticator
|
|
def get(self, vm, reqargs):
|
|
"""
|
|
Return the recent console log of {vm}
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: lines
|
|
type: integer
|
|
required: false
|
|
description: The number of lines to retrieve
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: VMLog
|
|
properties:
|
|
name:
|
|
type: string
|
|
description: The name of the VM
|
|
data:
|
|
type: string
|
|
description: The recent console log text
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_helper.vm_console(vm, reqargs.get("lines", None))
|
|
|
|
|
|
api.add_resource(API_VM_Console, "/vm/<vm>/console")
|
|
|
|
|
|
# /vm/<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/<vm>/rename")
|
|
|
|
|
|
# /vm/<vm>/device
|
|
class API_VM_Device(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "xml",
|
|
"required": True,
|
|
"helptext": "A Libvirt XML device document must be specified",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Hot-attach device XML to {vm}
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: xml
|
|
type: string
|
|
required: true
|
|
description: The raw Libvirt XML definition of the device to attach
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
try:
|
|
xml = reqargs.get("xml", None)
|
|
lxml_fromstring(xml)
|
|
except Exception:
|
|
return {"message": "Specified XML document is not valid"}, 400
|
|
|
|
vm_node_detail, retcode = api_helper.vm_node(vm)
|
|
if retcode == 200:
|
|
vm_node = vm_node_detail["node"]
|
|
else:
|
|
return vm_node_detail, retcode
|
|
|
|
task = run_celery_task("vm.device_attach", domain=vm, xml=xml, run_on=vm_node)
|
|
|
|
return (
|
|
{"task_id": task.id, "task_name": "vm.device_attach", "run_on": vm_node},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "xml",
|
|
"required": True,
|
|
"helptext": "A Libvirt XML device document must be specified",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def delete(self, vm, reqargs):
|
|
"""
|
|
Hot-detach device XML to {vm}
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: xml
|
|
type: string
|
|
required: true
|
|
description: The raw Libvirt XML definition of the device to detach
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
try:
|
|
xml = reqargs.get("xml", None)
|
|
lxml_fromstring(xml)
|
|
except Exception:
|
|
return {"message": "Specified XML document is not valid"}, 400
|
|
|
|
vm_node_detail, retcode = api_helper.vm_node(vm)
|
|
if retcode == 200:
|
|
vm_node = vm_node_detail["node"]
|
|
else:
|
|
return vm_node_detail, retcode
|
|
|
|
task = run_celery_task("vm.device_detach", domain=vm, xml=xml, run_on=vm_node)
|
|
|
|
return (
|
|
{"task_id": task.id, "task_name": "vm.device_detach", "run_on": vm_node},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Device, "/vm/<vm>/device")
|
|
|
|
|
|
# /vm/<vm>/backup
|
|
class API_VM_Backup(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "backup_path",
|
|
"required": True,
|
|
"helptext": "A local filesystem path on the primary coordinator must be specified",
|
|
},
|
|
{
|
|
"name": "incremental_parent",
|
|
"required": False,
|
|
},
|
|
{
|
|
"name": "retain_snapshot",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Create a backup of {vm} and its volumes to a local primary coordinator filesystem path
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: backup_path
|
|
type: string
|
|
required: true
|
|
description: A local filesystem path on the primary coordinator to store the backup
|
|
- in: query
|
|
name: incremental_parent
|
|
type: string
|
|
required: false
|
|
description: A previous backup datestamp to use as an incremental parent; if unspecified a full backup is taken
|
|
- in: query
|
|
name: retain_snapshot
|
|
type: boolean
|
|
required: false
|
|
default: false
|
|
description: Whether or not to retain this backup's volume snapshots to use as a future incremental parent; full backups only
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
backup_path = reqargs.get("backup_path", None)
|
|
incremental_parent = reqargs.get("incremental_parent", None)
|
|
retain_snapshot = bool(strtobool(reqargs.get("retain_snapshot", "false")))
|
|
return api_helper.vm_backup(
|
|
vm, backup_path, incremental_parent, retain_snapshot
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "backup_path",
|
|
"required": True,
|
|
"helptext": "A local filesystem path on the primary coordinator must be specified",
|
|
},
|
|
{
|
|
"name": "backup_datestring",
|
|
"required": True,
|
|
"helptext": "A backup datestring must be specified",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def delete(self, vm, reqargs):
|
|
"""
|
|
Remove a backup of {vm}, including snapshots, from a local primary coordinator filesystem path
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: backup_path
|
|
type: string
|
|
required: true
|
|
description: A local filesystem path on the primary coordinator where the backup is stored
|
|
- in: query
|
|
name: backup_datestring
|
|
type: string
|
|
required: true
|
|
description: The backup datestring identifier (e.g. 20230102030405)
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
backup_path = reqargs.get("backup_path", None)
|
|
backup_datestring = reqargs.get("backup_datestring", None)
|
|
return api_helper.vm_remove_backup(vm, backup_path, backup_datestring)
|
|
|
|
|
|
api.add_resource(API_VM_Backup, "/vm/<vm>/backup")
|
|
|
|
|
|
# /vm/<vm>/restore
|
|
class API_VM_Restore(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "backup_path",
|
|
"required": True,
|
|
"helptext": "A local filesystem path on the primary coordinator must be specified",
|
|
},
|
|
{
|
|
"name": "backup_datestring",
|
|
"required": True,
|
|
"helptext": "A backup datestring must be specified",
|
|
},
|
|
{
|
|
"name": "retain_snapshot",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Restore a backup of {vm} and its volumes from a local primary coordinator filesystem path
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: backup_path
|
|
type: string
|
|
required: true
|
|
description: A local filesystem path on the primary coordinator where the backup is stored
|
|
- in: query
|
|
name: backup_datestring
|
|
type: string
|
|
required: true
|
|
description: The backup datestring identifier (e.g. 20230102030405)
|
|
- in: query
|
|
name: retain_snapshot
|
|
type: boolean
|
|
required: false
|
|
default: true
|
|
description: Whether or not to retain the (parent, if incremental) volume snapshot after restore
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
backup_path = reqargs.get("backup_path", None)
|
|
backup_datestring = reqargs.get("backup_datestring", None)
|
|
retain_snapshot = bool(strtobool(reqargs.get("retain_snapshot", "true")))
|
|
return api_helper.vm_restore(
|
|
vm, backup_path, backup_datestring, retain_snapshot
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Restore, "/vm/<vm>/restore")
|
|
|
|
|
|
# /vm/<vm>/snapshot
|
|
class API_VM_Snapshot(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "snapshot_name",
|
|
"required": False,
|
|
"helptext": "",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Take a snapshot of a VM's disks and configuration
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: snapshot_name
|
|
type: string
|
|
required: false
|
|
description: A custom name for the snapshot instead of autogeneration by date
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
snapshot_name = reqargs.get("snapshot_name", None)
|
|
|
|
task = run_celery_task(
|
|
"vm.create_snapshot",
|
|
domain=vm,
|
|
snapshot_name=snapshot_name,
|
|
run_on="primary",
|
|
)
|
|
|
|
return (
|
|
{
|
|
"task_id": task.id,
|
|
"task_name": "vm.create_snapshot",
|
|
"run_on": f"{get_primary_node()} (primary)",
|
|
},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "snapshot_name",
|
|
"required": True,
|
|
"helptext": "A snapshot name must be specified",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def delete(self, vm, reqargs):
|
|
"""
|
|
Remove a snapshot of a VM's disks and configuration
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: snapshot_name
|
|
type: string
|
|
required: true
|
|
description: The name of the snapshot to remove
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
snapshot_name = reqargs.get("snapshot_name", None)
|
|
|
|
task = run_celery_task(
|
|
"vm.remove_snapshot",
|
|
domain=vm,
|
|
snapshot_name=snapshot_name,
|
|
run_on="primary",
|
|
)
|
|
|
|
return (
|
|
{
|
|
"task_id": task.id,
|
|
"task_name": "vm.remove_snapshot",
|
|
"run_on": f"{get_primary_node()} (primary)",
|
|
},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Snapshot, "/vm/<vm>/snapshot")
|
|
|
|
|
|
# /vm/<vm>/snapshot/rollback
|
|
class API_VM_Snapshot_Rollback(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "snapshot_name",
|
|
"required": True,
|
|
"helptext": "A snapshot name must be specified",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Roll back to a snapshot of a VM's disks and configuration
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: snapshot_name
|
|
type: string
|
|
required: true
|
|
description: The name of the snapshot to roll back to
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
snapshot_name = reqargs.get("snapshot_name", None)
|
|
|
|
task = run_celery_task(
|
|
"vm.rollback_snapshot",
|
|
domain=vm,
|
|
snapshot_name=snapshot_name,
|
|
run_on="primary",
|
|
)
|
|
|
|
return (
|
|
{
|
|
"task_id": task.id,
|
|
"task_name": "vm.rollback_snapshot",
|
|
"run_on": f"{get_primary_node()} (primary)",
|
|
},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Snapshot_Rollback, "/vm/<vm>/snapshot/rollback")
|
|
|
|
|
|
# /vm/<vm>/snapshot/export
|
|
class API_VM_Snapshot_Export(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "snapshot_name",
|
|
"required": True,
|
|
"helptext": "A snapshot name must be specified",
|
|
},
|
|
{
|
|
"name": "export_path",
|
|
"required": True,
|
|
"helptext": "An absolute directory path on the PVC primary coordinator to export files to",
|
|
},
|
|
{
|
|
"name": "incremental_parent",
|
|
"required": False,
|
|
"helptext": "A snapshot name to generate an incremental diff from",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Export a snapshot of a VM's disks and configuration to files
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: snapshot_name
|
|
type: string
|
|
required: true
|
|
description: The name of the snapshot to export (must exist)
|
|
- in: query
|
|
name: export_path
|
|
type: string (path)
|
|
required: true
|
|
description: The absolute file path to export the snapshot to on the active primary coordinator
|
|
- in: query
|
|
name: incremental_parent
|
|
type: string
|
|
required: false
|
|
description: A snapshot name to generate an incremental diff from
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
snapshot_name = reqargs.get("snapshot_name", None)
|
|
export_path = reqargs.get("export_path", None)
|
|
incremental_parent = reqargs.get("incremental_parent", None)
|
|
|
|
task = run_celery_task(
|
|
"vm.export_snapshot",
|
|
domain=vm,
|
|
snapshot_name=snapshot_name,
|
|
export_path=export_path,
|
|
incremental_parent=incremental_parent,
|
|
run_on="primary",
|
|
)
|
|
|
|
return (
|
|
{
|
|
"task_id": task.id,
|
|
"task_name": "vm.export_snapshot",
|
|
"run_on": f"{get_primary_node()} (primary)",
|
|
},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Snapshot_Export, "/vm/<vm>/snapshot/export")
|
|
|
|
|
|
# /vm/<vm>/snapshot/import
|
|
class API_VM_Snapshot_Import(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "snapshot_name",
|
|
"required": True,
|
|
"helptext": "A snapshot name must be specified",
|
|
},
|
|
{
|
|
"name": "import_path",
|
|
"required": True,
|
|
"helptext": "An absolute directory path on the PVC primary coordinator to import files from",
|
|
},
|
|
{
|
|
"name": "retain_snapshot",
|
|
"required": False,
|
|
"helptext": "Whether to retain the snapshot of the import or not (default: true)",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Import a snapshot of a VM's disks and configuration from files
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: snapshot_name
|
|
type: string
|
|
required: true
|
|
description: The name of the snapshot to roll back to
|
|
- in: query
|
|
name: import_path
|
|
type: string (path)
|
|
required: true
|
|
description: The absolute file path to import the snapshot from on the active primary coordinator
|
|
- in: query
|
|
name: retain_snapshot
|
|
type: boolean
|
|
required: false
|
|
default: true
|
|
description: Whether or not to retain the (parent, if incremental) volume snapshot after restore
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
snapshot_name = reqargs.get("snapshot_name", None)
|
|
import_path = reqargs.get("import_path", None)
|
|
retain_snapshot = bool(strtobool(reqargs.get("retain_snapshot", "True")))
|
|
|
|
task = run_celery_task(
|
|
"vm.import_snapshot",
|
|
domain=vm,
|
|
snapshot_name=snapshot_name,
|
|
import_path=import_path,
|
|
retain_snapshot=retain_snapshot,
|
|
run_on="primary",
|
|
)
|
|
|
|
return (
|
|
{
|
|
"task_id": task.id,
|
|
"task_name": "vm.import_snapshot",
|
|
"run_on": f"{get_primary_node()} (primary)",
|
|
},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Snapshot_Import, "/vm/<vm>/snapshot/import")
|
|
|
|
|
|
# /vm/<vm>/snapshot/send
|
|
class API_VM_Snapshot_Send(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "snapshot_name",
|
|
"required": True,
|
|
"helptext": "A snapshot name must be specified",
|
|
},
|
|
{
|
|
"name": "destination_api_uri",
|
|
"required": True,
|
|
"helptext": "A destination API URI must be specified",
|
|
},
|
|
{
|
|
"name": "destination_api_key",
|
|
"required": True,
|
|
"helptext": "A destination API key must be specified",
|
|
},
|
|
{
|
|
"name": "destination_api_verify_ssl",
|
|
"required": False,
|
|
},
|
|
{
|
|
"name": "incremental_parent",
|
|
"required": False,
|
|
},
|
|
{
|
|
"name": "destination_storage_pool",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Send a snapshot of a VM's disks and configuration to another PVC cluster
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: snapshot_name
|
|
type: string
|
|
required: true
|
|
description: The name of the snapshot to export (must exist)
|
|
- in: query
|
|
name: destination_api_uri
|
|
type: string
|
|
required: true
|
|
description: The base API URI of the destination PVC cluster (with prefix if applicable)
|
|
- in: query
|
|
name: destination_api_key
|
|
type: string
|
|
required: true
|
|
description: The API authentication key of the destination PVC cluster
|
|
- in: query
|
|
name: destination_api_verify_ssl
|
|
type: boolean
|
|
required: false
|
|
default: true
|
|
description: Whether or not to validate SSL certificates for an SSL-enabled destination API
|
|
- in: query
|
|
name: incremental_parent
|
|
type: string
|
|
required: false
|
|
description: A snapshot name to generate an incremental diff from; incremental send only if unset
|
|
- in: query
|
|
name: destination_storage_pool
|
|
type: string
|
|
required: false
|
|
default: source storage pool name
|
|
description: The remote cluster storage pool to create RBD volumes in, if different from the source storage pool
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
snapshot_name = reqargs.get("snapshot_name", None)
|
|
destination_api_uri = reqargs.get("destination_api_uri", None)
|
|
destination_api_key = reqargs.get("destination_api_key", None)
|
|
destination_api_verify_ssl = bool(
|
|
strtobool(reqargs.get("destination_api_verify_ssl", "true"))
|
|
)
|
|
incremental_parent = reqargs.get("incremental_parent", None)
|
|
destination_storage_pool = reqargs.get("destination_storage_pool", None)
|
|
|
|
task = run_celery_task(
|
|
"vm.send_snapshot",
|
|
domain=vm,
|
|
snapshot_name=snapshot_name,
|
|
destination_api_uri=destination_api_uri,
|
|
destination_api_key=destination_api_key,
|
|
destination_api_verify_ssl=destination_api_verify_ssl,
|
|
incremental_parent=incremental_parent,
|
|
destination_storage_pool=destination_storage_pool,
|
|
run_on="primary",
|
|
)
|
|
|
|
return (
|
|
{
|
|
"task_id": task.id,
|
|
"task_name": "vm.send_snapshot",
|
|
"run_on": f"{get_primary_node()} (primary)",
|
|
},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Snapshot_Send, "/vm/<vm>/snapshot/send")
|
|
|
|
|
|
# /vm/<vm>/snapshot/receive/block
|
|
class API_VM_Snapshot_Receive_Block(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "pool",
|
|
"required": True,
|
|
},
|
|
{
|
|
"name": "volume",
|
|
"required": True,
|
|
},
|
|
{
|
|
"name": "snapshot",
|
|
"required": True,
|
|
},
|
|
{
|
|
"name": "size",
|
|
"required": True,
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Receive a full snapshot of a single RBD volume from another PVC cluster
|
|
|
|
NOTICE: This is an API-internal endpoint used by /vm/<vm>/snapshot/send; it should never be called by a client.
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: pool
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD data pool
|
|
- in: query
|
|
name: volume
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD volume
|
|
- in: query
|
|
name: snapshot
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD volume snapshot
|
|
- in: query
|
|
name: size
|
|
type: integer
|
|
required: true
|
|
description: The size in bytes of the Ceph RBD volume
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_helper.vm_snapshot_receive_block_full(
|
|
reqargs.get("pool"),
|
|
reqargs.get("volume"),
|
|
reqargs.get("snapshot"),
|
|
int(reqargs.get("size")),
|
|
flask.request,
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "pool",
|
|
"required": True,
|
|
},
|
|
{
|
|
"name": "volume",
|
|
"required": True,
|
|
},
|
|
{
|
|
"name": "snapshot",
|
|
"required": True,
|
|
},
|
|
{
|
|
"name": "source_snapshot",
|
|
"required": True,
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def put(self, vm, reqargs):
|
|
"""
|
|
Receive a single diff element from a snapshot of a single RBD volume from another PVC cluster
|
|
|
|
NOTICE: This is an API-internal endpoint used by /vm/<vm>/snapshot/send; it should never be called by a client.
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: pool
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD data pool
|
|
- in: query
|
|
name: volume
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD volume
|
|
- in: query
|
|
name: snapshot
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD volume snapshot
|
|
- in: query
|
|
name: source_snapshot
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD volume snapshot parent
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_helper.vm_snapshot_receive_block_diff(
|
|
reqargs.get("pool"),
|
|
reqargs.get("volume"),
|
|
reqargs.get("snapshot"),
|
|
reqargs.get("source_snapshot"),
|
|
flask.request,
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "pool",
|
|
"required": True,
|
|
},
|
|
{
|
|
"name": "volume",
|
|
"required": True,
|
|
},
|
|
{
|
|
"name": "snapshot",
|
|
"required": True,
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def patch(self, vm, reqargs):
|
|
"""
|
|
Create the block snapshot at snapshot of volume
|
|
|
|
NOTICE: This is an API-internal endpoint used by /vm/<vm>/snapshot/send; it should never be called by a client.
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: pool
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD data pool
|
|
- in: query
|
|
name: volume
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD volume
|
|
- in: query
|
|
name: snapshot
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD volume snapshot
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_helper.vm_snapshot_receive_block_createsnap(
|
|
reqargs.get("pool"),
|
|
reqargs.get("volume"),
|
|
reqargs.get("snapshot"),
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Snapshot_Receive_Block, "/vm/<vm>/snapshot/receive/block")
|
|
|
|
|
|
# /vm/<vm>/snapshot/receive/config
|
|
class API_VM_Snapshot_Receive_Config(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "snapshot",
|
|
"required": True,
|
|
},
|
|
{
|
|
"name": "source_snapshot",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Receive a snapshot of a VM configuration from another PVC cluster
|
|
|
|
NOTICE: This is an API-internal endpoint used by /vm/<vm>/snapshot/send; it should never be called by a client.
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: pool
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD data pool
|
|
- in: query
|
|
name: volume
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD volume
|
|
- in: query
|
|
name: snapshot
|
|
type: string
|
|
required: true
|
|
description: The name of the destination Ceph RBD volume snapshot
|
|
- in: query
|
|
name: size
|
|
type: integer
|
|
required: true
|
|
description: The size in bytes of the Ceph RBD volume
|
|
- in: query
|
|
name: source_snapshot
|
|
type: string
|
|
required: false
|
|
description: The name of the destination Ceph RBD volume snapshot parent for incremental transfers
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_helper.vm_snapshot_receive_config(
|
|
reqargs.get("snapshot"),
|
|
flask.request.get_json(),
|
|
source_snapshot=reqargs.get("source_snapshot"),
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Snapshot_Receive_Config, "/vm/<vm>/snapshot/receive/config")
|
|
|
|
|
|
# /vm/<vm>/mirror/create
|
|
class API_VM_Mirror_Create(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "destination_api_uri",
|
|
"required": True,
|
|
"helptext": "A destination API URI must be specified",
|
|
},
|
|
{
|
|
"name": "destination_api_key",
|
|
"required": True,
|
|
"helptext": "A destination API key must be specified",
|
|
},
|
|
{
|
|
"name": "destination_api_verify_ssl",
|
|
"required": False,
|
|
},
|
|
{
|
|
"name": "destination_storage_pool",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Create (or update) a snapshot mirror of a VM to a remote cluster
|
|
|
|
This method handles both the creation of a new VM snapshot, as well as sending that snapshot to a remote cluster, creating or updating a VM mirror. It will also automatically handle full vs. incremental block sends if possible based on the available snapshots on both sides.
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: destination_api_uri
|
|
type: string
|
|
required: true
|
|
description: The base API URI of the destination PVC cluster (with prefix if applicable)
|
|
- in: query
|
|
name: destination_api_key
|
|
type: string
|
|
required: true
|
|
description: The API authentication key of the destination PVC cluster
|
|
- in: query
|
|
name: destination_api_verify_ssl
|
|
type: boolean
|
|
required: false
|
|
default: true
|
|
description: Whether or not to validate SSL certificates for an SSL-enabled destination API
|
|
- in: query
|
|
name: destination_storage_pool
|
|
type: string
|
|
required: false
|
|
default: source storage pool name
|
|
description: The remote cluster storage pool to create RBD volumes in, if different from the source storage pool
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
destination_api_uri = reqargs.get("destination_api_uri", None)
|
|
destination_api_key = reqargs.get("destination_api_key", None)
|
|
destination_api_verify_ssl = bool(
|
|
strtobool(reqargs.get("destination_api_verify_ssl", "true"))
|
|
)
|
|
destination_storage_pool = reqargs.get("destination_storage_pool", None)
|
|
|
|
task = run_celery_task(
|
|
"vm.create_mirror",
|
|
domain=vm,
|
|
destination_api_uri=destination_api_uri,
|
|
destination_api_key=destination_api_key,
|
|
destination_api_verify_ssl=destination_api_verify_ssl,
|
|
destination_storage_pool=destination_storage_pool,
|
|
run_on="primary",
|
|
)
|
|
|
|
return (
|
|
{
|
|
"task_id": task.id,
|
|
"task_name": "vm.send_snapshot",
|
|
"run_on": f"{get_primary_node()} (primary)",
|
|
},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Mirror_Create, "/vm/<vm>/mirror/create")
|
|
|
|
|
|
# /vm/<vm>/mirror/promote
|
|
class API_VM_Mirror_Promote(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "destination_api_uri",
|
|
"required": True,
|
|
"helptext": "A destination API URI must be specified",
|
|
},
|
|
{
|
|
"name": "destination_api_key",
|
|
"required": True,
|
|
"helptext": "A destination API key must be specified",
|
|
},
|
|
{
|
|
"name": "destination_api_verify_ssl",
|
|
"required": False,
|
|
},
|
|
{
|
|
"name": "destination_storage_pool",
|
|
"required": False,
|
|
},
|
|
{
|
|
"name": "remove_on_source",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, vm, reqargs):
|
|
"""
|
|
Promote a snapshot mirror of a VM on a remote cluster, flipping "mirror" state, and optionally removing the source VM on this cluster.
|
|
|
|
This method handles shutting down the VM on this cluster, creating a new VM snapshot, sending that snapshot to a remote cluster, then starting up the VM on the remote cluster. It will also automatically handle full vs. incremental block sends if possible based on the available snapshots on both sides.
|
|
|
|
NOTE: This method may be used alone to perform a one-shot cross-cluster move; creating the mirror first is not required, though doing so will improve performance by allowing an incremental block send.
|
|
---
|
|
tags:
|
|
- vm
|
|
parameters:
|
|
- in: query
|
|
name: destination_api_uri
|
|
type: string
|
|
required: true
|
|
description: The base API URI of the destination PVC cluster (with prefix if applicable)
|
|
- in: query
|
|
name: destination_api_key
|
|
type: string
|
|
required: true
|
|
description: The API authentication key of the destination PVC cluster
|
|
- in: query
|
|
name: destination_api_verify_ssl
|
|
type: boolean
|
|
required: false
|
|
default: true
|
|
description: Whether or not to validate SSL certificates for an SSL-enabled destination API
|
|
- in: query
|
|
name: destination_storage_pool
|
|
type: string
|
|
required: false
|
|
default: source storage pool name
|
|
description: The remote cluster storage pool to create RBD volumes in, if different from the source storage pool
|
|
- in: query
|
|
name: remove_on_source
|
|
required: false
|
|
default: false
|
|
description: Remove the VM on the source cluster once promoted (performs a full move between clusters)
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Execution error
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
destination_api_uri = reqargs.get("destination_api_uri", None)
|
|
destination_api_key = reqargs.get("destination_api_key", None)
|
|
destination_api_verify_ssl = bool(
|
|
strtobool(reqargs.get("destination_api_verify_ssl", "true"))
|
|
)
|
|
destination_storage_pool = reqargs.get("destination_storage_pool", None)
|
|
remove_on_source = bool(strtobool(reqargs.get("remove_on_source", "false")))
|
|
|
|
task = run_celery_task(
|
|
"vm.promote_mirror",
|
|
domain=vm,
|
|
destination_api_uri=destination_api_uri,
|
|
destination_api_key=destination_api_key,
|
|
destination_api_verify_ssl=destination_api_verify_ssl,
|
|
destination_storage_pool=destination_storage_pool,
|
|
remove_on_source=remove_on_source,
|
|
run_on="primary",
|
|
)
|
|
|
|
return (
|
|
{
|
|
"task_id": task.id,
|
|
"task_name": "vm.send_snapshot",
|
|
"run_on": f"{get_primary_node()} (primary)",
|
|
},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Mirror_Promote, "/vm/<vm>/mirror/promote")
|
|
|
|
|
|
# /vm/autobackup
|
|
class API_VM_Autobackup_Root(Resource):
|
|
@RequestParser(
|
|
[
|
|
{"name": "force_full"},
|
|
{"name": "email_recipients"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, reqargs):
|
|
"""
|
|
Trigger a cluster autobackup job
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: force_full
|
|
type: boolean
|
|
required: false
|
|
description: If set and true, triggers a full autobackup regardless of schedule
|
|
- in: query
|
|
name: email_recipients
|
|
type: array
|
|
description: A list of email addresses to send failure and report emails to
|
|
items:
|
|
type: string
|
|
example: "user@domain.tld"
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
|
|
email_recipients = reqargs.get("email_recipients", None)
|
|
if email_recipients is not None and not isinstance(email_recipients, list):
|
|
email_recipients = [email_recipients]
|
|
|
|
task = run_celery_task(
|
|
"cluster.autobackup",
|
|
force_full=bool(strtobool(reqargs.get("force_full", "false"))),
|
|
email_recipients=email_recipients,
|
|
run_on="primary",
|
|
)
|
|
return (
|
|
{
|
|
"task_id": task.id,
|
|
"task_name": "cluster.autobackup",
|
|
"run_on": f"{get_primary_node()} (primary)",
|
|
},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_VM_Autobackup_Root, "/vm/autobackup")
|
|
|
|
|
|
##########################################################
|
|
# 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
|
|
mtu:
|
|
type: integer
|
|
description: The MTU of the network, if set; empty otherwise
|
|
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": "mtu"},
|
|
{"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: mtu
|
|
type: integer
|
|
description: The MTU of the network; defaults to the underlying interface MTU if not set
|
|
- 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("mtu", ""),
|
|
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/<vni>
|
|
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": "mtu"},
|
|
{"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: mtu
|
|
type: integer
|
|
description: The MTU of the network; defaults to the underlying interface MTU if not set
|
|
- 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("mtu", ""),
|
|
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": "mtu"},
|
|
{"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: mtu
|
|
type: integer
|
|
description: The MTU 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("mtu", 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/<vni>")
|
|
|
|
|
|
# /network/<vni>/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/<vni>/lease")
|
|
|
|
|
|
# /network/<vni>/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/<vni>/lease/<mac>")
|
|
|
|
|
|
# /network/<vni>/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/<vni>/acl")
|
|
|
|
|
|
# /network/<vni>/acl/<description>
|
|
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/<vni>/acl/<description>")
|
|
|
|
|
|
##########################################################
|
|
# Client API - SR-IOV
|
|
##########################################################
|
|
|
|
|
|
# /sriov
|
|
class API_SRIOV_Root(Resource):
|
|
@Authenticator
|
|
def get(self):
|
|
pass
|
|
|
|
|
|
api.add_resource(API_SRIOV_Root, "/sriov")
|
|
|
|
|
|
# /sriov/pf
|
|
class API_SRIOV_PF_Root(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "node",
|
|
"required": True,
|
|
"helptext": "A valid node must be specified.",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def get(self, reqargs):
|
|
"""
|
|
Return a list of SR-IOV PFs on a given node
|
|
---
|
|
tags:
|
|
- network / sriov
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: sriov_pf
|
|
properties:
|
|
phy:
|
|
type: string
|
|
description: The name of the SR-IOV PF device
|
|
mtu:
|
|
type: string
|
|
description: The MTU of the SR-IOV PF device
|
|
vfs:
|
|
type: list
|
|
items:
|
|
type: string
|
|
description: The PHY name of a VF of this PF
|
|
"""
|
|
return api_helper.sriov_pf_list(reqargs.get("node"))
|
|
|
|
|
|
api.add_resource(API_SRIOV_PF_Root, "/sriov/pf")
|
|
|
|
|
|
# /sriov/pf/<node>
|
|
class API_SRIOV_PF_Node(Resource):
|
|
@Authenticator
|
|
def get(self, node):
|
|
"""
|
|
Return a list of SR-IOV PFs on node {node}
|
|
---
|
|
tags:
|
|
- network / sriov
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/sriov_pf'
|
|
"""
|
|
return api_helper.sriov_pf_list(node)
|
|
|
|
|
|
api.add_resource(API_SRIOV_PF_Node, "/sriov/pf/<node>")
|
|
|
|
|
|
# /sriov/vf
|
|
class API_SRIOV_VF_Root(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "node",
|
|
"required": True,
|
|
"helptext": "A valid node must be specified.",
|
|
},
|
|
{
|
|
"name": "pf",
|
|
"required": False,
|
|
"helptext": "A PF parent may be specified.",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def get(self, reqargs):
|
|
"""
|
|
Return a list of SR-IOV VFs on a given node, optionally limited to those in the specified PF
|
|
---
|
|
tags:
|
|
- network / sriov
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: sriov_vf
|
|
properties:
|
|
phy:
|
|
type: string
|
|
description: The name of the SR-IOV VF device
|
|
pf:
|
|
type: string
|
|
description: The name of the SR-IOV PF parent of this VF device
|
|
mtu:
|
|
type: integer
|
|
description: The current MTU of the VF device
|
|
mac:
|
|
type: string
|
|
description: The current MAC address of the VF device
|
|
config:
|
|
type: object
|
|
id: sriov_vf_config
|
|
properties:
|
|
vlan_id:
|
|
type: string
|
|
description: The tagged vLAN ID of the SR-IOV VF device
|
|
vlan_qos:
|
|
type: string
|
|
description: The QOS group of the tagged vLAN
|
|
tx_rate_min:
|
|
type: string
|
|
description: The minimum TX rate of the SR-IOV VF device
|
|
tx_rate_max:
|
|
type: string
|
|
description: The maximum TX rate of the SR-IOV VF device
|
|
spoof_check:
|
|
type: boolean
|
|
description: Whether device spoof checking is enabled or disabled
|
|
link_state:
|
|
type: string
|
|
description: The current SR-IOV VF link state (either enabled, disabled, or auto)
|
|
trust:
|
|
type: boolean
|
|
description: Whether guest device trust is enabled or disabled
|
|
query_rss:
|
|
type: boolean
|
|
description: Whether VF RSS querying is enabled or disabled
|
|
usage:
|
|
type: object
|
|
id: sriov_vf_usage
|
|
properties:
|
|
used:
|
|
type: boolean
|
|
description: Whether the SR-IOV VF is currently used by a VM or not
|
|
domain:
|
|
type: boolean
|
|
description: The UUID of the domain the SR-IOV VF is currently used by
|
|
"""
|
|
return api_helper.sriov_vf_list(reqargs.get("node"), reqargs.get("pf", None))
|
|
|
|
|
|
api.add_resource(API_SRIOV_VF_Root, "/sriov/vf")
|
|
|
|
|
|
# /sriov/vf/<node>
|
|
class API_SRIOV_VF_Node(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "pf",
|
|
"required": False,
|
|
"helptext": "A PF parent may be specified.",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def get(self, node, reqargs):
|
|
"""
|
|
Return a list of SR-IOV VFs on node {node}, optionally limited to those in the specified PF
|
|
---
|
|
tags:
|
|
- network / sriov
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/sriov_vf'
|
|
"""
|
|
return api_helper.sriov_vf_list(node, reqargs.get("pf", None))
|
|
|
|
|
|
api.add_resource(API_SRIOV_VF_Node, "/sriov/vf/<node>")
|
|
|
|
|
|
# /sriov/vf/<node>/<vf>
|
|
class API_SRIOV_VF_Element(Resource):
|
|
@Authenticator
|
|
def get(self, node, vf):
|
|
"""
|
|
Return information about {vf} on {node}
|
|
---
|
|
tags:
|
|
- network / sriov
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/sriov_vf'
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
vf_list = list()
|
|
full_vf_list, _ = api_helper.sriov_vf_list(node)
|
|
for vf_element in full_vf_list:
|
|
if vf_element["phy"] == vf:
|
|
vf_list.append(vf_element)
|
|
|
|
if len(vf_list) == 1:
|
|
return vf_list, 200
|
|
else:
|
|
return {"message": "No VF '{}' found on node '{}'".format(vf, node)}, 404
|
|
|
|
@RequestParser(
|
|
[
|
|
{"name": "vlan_id"},
|
|
{"name": "vlan_qos"},
|
|
{"name": "tx_rate_min"},
|
|
{"name": "tx_rate_max"},
|
|
{
|
|
"name": "link_state",
|
|
"choices": ("auto", "enable", "disable"),
|
|
"helptext": "A valid state must be specified",
|
|
},
|
|
{"name": "spoof_check"},
|
|
{"name": "trust"},
|
|
{"name": "query_rss"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def put(self, node, vf, reqargs):
|
|
"""
|
|
Set the configuration of {vf} on {node}
|
|
---
|
|
tags:
|
|
- network / sriov
|
|
parameters:
|
|
- in: query
|
|
name: vlan_id
|
|
type: integer
|
|
required: false
|
|
description: The vLAN ID for vLAN tagging (0 is disabled)
|
|
- in: query
|
|
name: vlan_qos
|
|
type: integer
|
|
required: false
|
|
description: The vLAN QOS priority (0 is disabled)
|
|
- in: query
|
|
name: tx_rate_min
|
|
type: integer
|
|
required: false
|
|
description: The minimum TX rate (0 is disabled)
|
|
- in: query
|
|
name: tx_rate_max
|
|
type: integer
|
|
required: false
|
|
description: The maximum TX rate (0 is disabled)
|
|
- in: query
|
|
name: link_state
|
|
type: string
|
|
required: false
|
|
description: The administrative link state
|
|
enum:
|
|
- auto
|
|
- enable
|
|
- disable
|
|
- in: query
|
|
name: spoof_check
|
|
type: boolean
|
|
required: false
|
|
description: Enable or disable spoof checking
|
|
- in: query
|
|
name: trust
|
|
type: boolean
|
|
required: false
|
|
description: Enable or disable VF user trust
|
|
- in: query
|
|
name: query_rss
|
|
type: boolean
|
|
required: false
|
|
description: Enable or disable query RSS support
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_helper.update_sriov_vf_config(
|
|
node,
|
|
vf,
|
|
reqargs.get("vlan_id", None),
|
|
reqargs.get("vlan_qos", None),
|
|
reqargs.get("tx_rate_min", None),
|
|
reqargs.get("tx_rate_max", None),
|
|
reqargs.get("link_state", None),
|
|
reqargs.get("spoof_check", None),
|
|
reqargs.get("trust", None),
|
|
reqargs.get("query_rss", None),
|
|
)
|
|
|
|
|
|
api.add_resource(API_SRIOV_VF_Element, "/sriov/vf/<node>/<vf>")
|
|
|
|
|
|
##########################################################
|
|
# 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
|
|
test_format:
|
|
type: integer
|
|
description: The PVC benchmark format of the results
|
|
benchmark_result:
|
|
type: object
|
|
description: A benchmark test result; format not documented due to complexity
|
|
"""
|
|
return list_benchmarks(config, reqargs.get("job", None))
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "pool",
|
|
"required": True,
|
|
"helptext": "A valid pool must be specified.",
|
|
},
|
|
{
|
|
"name": "name",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@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
|
|
- in: query
|
|
name: name
|
|
type: string
|
|
required: false
|
|
description: An optional override name for the job
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
# 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_celery_task(
|
|
"storage.benchmark",
|
|
pool=reqargs.get("pool", None),
|
|
name=reqargs.get("name", None),
|
|
run_on="primary",
|
|
)
|
|
return (
|
|
{
|
|
"task_id": task.id,
|
|
"task_name": "storage.benchmark",
|
|
"run_on": f"{get_primary_node()} (primary)",
|
|
},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, 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/osddb
|
|
class API_Storage_Ceph_OSDDB_Root(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "node",
|
|
"required": True,
|
|
"helptext": "A valid node must be specified.",
|
|
},
|
|
{
|
|
"name": "device",
|
|
"required": True,
|
|
"helptext": "A valid device or detect string must be specified.",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, reqargs):
|
|
"""
|
|
Add a Ceph OSD database volume group 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 DB volume group on
|
|
- in: query
|
|
name: device
|
|
type: string
|
|
required: true
|
|
description: The block device (e.g. "/dev/sdb", "/dev/disk/by-path/...", etc.) or detect string ("detect:NAME:SIZE:ID") to create the OSD DB volume group on
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
node = reqargs.get("node", None)
|
|
|
|
task = run_celery_task(
|
|
"osd.add_db_vg", device=reqargs.get("device", None), run_on=node
|
|
)
|
|
|
|
return (
|
|
{"task_id": task.id, "task_name": "osd.add_db_vg", "run_on": node},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_Storage_Ceph_OSDDB_Root, "/storage/ceph/osddb")
|
|
|
|
|
|
# /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
|
|
device:
|
|
type: string
|
|
description: The OSD data block device
|
|
db_device:
|
|
type: string
|
|
description: The OSD database/WAL block device (logical volume); empty if not applicable
|
|
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 or detect string must be specified.",
|
|
},
|
|
{
|
|
"name": "weight",
|
|
"required": True,
|
|
"helptext": "An OSD weight must be specified.",
|
|
},
|
|
{
|
|
"name": "ext_db_ratio",
|
|
"required": False,
|
|
},
|
|
{
|
|
"name": "ext_db_size",
|
|
"required": False,
|
|
},
|
|
{
|
|
"name": "osd_count",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, reqargs):
|
|
"""
|
|
Add a Ceph OSD to the cluster
|
|
Note: This task may take up to 60s 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.) or detect string ("detect:NAME:SIZE:ID") to create the OSD on
|
|
- in: query
|
|
name: weight
|
|
type: number
|
|
required: true
|
|
description: The Ceph CRUSH weight for the OSD
|
|
- in: query
|
|
name: ext_db_ratio
|
|
type: float
|
|
required: false
|
|
description: If set, creates an OSD DB LV with this decimal ratio of DB to total OSD size (usually 0.05 i.e. 5%); mutually exclusive with ext_db_size
|
|
- in: query
|
|
name: ext_db_size
|
|
type: float
|
|
required: false
|
|
description: If set, creates an OSD DB LV with this explicit size in human units (e.g. 1024M, 20G); mutually exclusive with ext_db_ratio
|
|
- in: query
|
|
name: osd_count
|
|
type: integer
|
|
required: false
|
|
description: If set, create this many OSDs on the block device instead of 1; usually 2 or 4 depending on size
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
node = reqargs.get("node", None)
|
|
|
|
task = run_celery_task(
|
|
"osd.add",
|
|
device=reqargs.get("device", None),
|
|
weight=reqargs.get("weight", None),
|
|
ext_db_ratio=reqargs.get("ext_db_ratio", None),
|
|
ext_db_size=reqargs.get("ext_db_size", None),
|
|
split_count=reqargs.get("osd_count", None),
|
|
run_on=node,
|
|
)
|
|
|
|
return (
|
|
{"task_id": task.id, "task_name": "osd.add", "run_on": node},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_Storage_Ceph_OSD_Root, "/storage/ceph/osd")
|
|
|
|
|
|
# /storage/ceph/osd/<osdid>
|
|
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": "new_device",
|
|
"required": True,
|
|
"helptext": "A valid device or detect string must be specified.",
|
|
},
|
|
{
|
|
"name": "old_device",
|
|
"required": False,
|
|
},
|
|
{
|
|
"name": "weight",
|
|
"required": False,
|
|
},
|
|
{
|
|
"name": "ext_db_ratio",
|
|
"required": False,
|
|
},
|
|
{
|
|
"name": "ext_db_size",
|
|
"required": False,
|
|
},
|
|
{
|
|
"name": "yes-i-really-mean-it",
|
|
"required": True,
|
|
"helptext": "Please confirm that 'yes-i-really-mean-it'.",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, osdid, reqargs):
|
|
"""
|
|
Replace a Ceph OSD in the cluster
|
|
Note: This task may take up to 30s to complete and return
|
|
---
|
|
tags:
|
|
- storage / ceph
|
|
parameters:
|
|
- in: query
|
|
name: new_device
|
|
type: string
|
|
required: true
|
|
description: The block device (e.g. "/dev/sdb", "/dev/disk/by-path/...", etc.) or detect string ("detect:NAME:SIZE:ID") to replace the OSD onto
|
|
- in: query
|
|
name: old_device
|
|
type: string
|
|
required: false
|
|
description: The block device (e.g. "/dev/sdb", "/dev/disk/by-path/...", etc.) or detect string ("detect:NAME:SIZE:ID") of the original OSD
|
|
- in: query
|
|
name: weight
|
|
type: number
|
|
required: false
|
|
description: The Ceph CRUSH weight for the replacement OSD
|
|
- in: query
|
|
name: ext_db_ratio
|
|
type: float
|
|
required: false
|
|
description: If set, creates an OSD DB LV for the replcement OSD with this decimal ratio of DB to total OSD size (usually 0.05 i.e. 5%); if unset, use existing ext_db_size
|
|
- in: query
|
|
name: ext_db_size
|
|
type: float
|
|
required: false
|
|
description: If set, creates an OSD DB LV for the replacement OSD with this explicit size in human units (e.g. 1024M, 20G); if unset, use existing ext_db_size
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
osd_node_detail, retcode = api_helper.ceph_osd_node(osdid)
|
|
if retcode == 200:
|
|
node = osd_node_detail["node"]
|
|
else:
|
|
return osd_node_detail, retcode
|
|
|
|
task = run_celery_task(
|
|
"osd.replace",
|
|
osd_id=osdid,
|
|
new_device=reqargs.get("new_device"),
|
|
old_device=reqargs.get("old_device", None),
|
|
weight=reqargs.get("weight", None),
|
|
ext_db_ratio=reqargs.get("ext_db_ratio", None),
|
|
ext_db_size=reqargs.get("ext_db_size", None),
|
|
run_on=node,
|
|
)
|
|
|
|
return (
|
|
{"task_id": task.id, "task_name": "osd.replace", "run_on": node},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "device",
|
|
"required": True,
|
|
"helptext": "A valid device or detect string must be specified.",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def put(self, osdid, reqargs):
|
|
"""
|
|
Refresh (reimport) a Ceph OSD in the cluster
|
|
Note: This task may take up to 30s to complete and return
|
|
---
|
|
tags:
|
|
- storage / ceph
|
|
parameters:
|
|
- in: query
|
|
name: device
|
|
type: string
|
|
required: true
|
|
description: The block device (e.g. "/dev/sdb", "/dev/disk/by-path/...", etc.) or detect string ("detect:NAME:SIZE:ID") that the OSD should be using
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
osd_node_detail, retcode = api_helper.ceph_osd_node(osdid)
|
|
if retcode == 200:
|
|
node = osd_node_detail["node"]
|
|
else:
|
|
return osd_node_detail, retcode
|
|
|
|
task = run_celery_task(
|
|
"osd.refresh",
|
|
osd_id=osdid,
|
|
device=reqargs.get("device", None),
|
|
ext_db_flag=False,
|
|
run_on=node,
|
|
)
|
|
|
|
return (
|
|
{"task_id": task.id, "task_name": "osd.refresh", "run_on": node},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "force",
|
|
"required": False,
|
|
"helptext": "Force removal even if steps fail.",
|
|
},
|
|
{
|
|
"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: force
|
|
type: boolean
|
|
required: false
|
|
description: Force removal even if some step(s) fail
|
|
- 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:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
osd_node_detail, retcode = api_helper.ceph_osd_node(osdid)
|
|
if retcode == 200:
|
|
node = osd_node_detail["node"]
|
|
else:
|
|
return osd_node_detail, retcode
|
|
|
|
task = run_celery_task(
|
|
"osd.remove",
|
|
osd_id=osdid,
|
|
force_flag=reqargs.get("force", False),
|
|
run_on=node,
|
|
)
|
|
|
|
return (
|
|
{"task_id": task.id, "task_name": "osd.remove", "run_on": node},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_Storage_Ceph_OSD_Element, "/storage/ceph/osd/<osdid>")
|
|
|
|
|
|
# /storage/ceph/osd/<osdid>/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/<osdid>/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
|
|
tier:
|
|
type: string
|
|
description: The device class/tier of the pool
|
|
pgs:
|
|
type: integer
|
|
description: The number of PGs (placement groups) for 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.",
|
|
},
|
|
{
|
|
"name": "tier",
|
|
"required": False,
|
|
"choices": ("hdd", "ssd", "nvme", "default"),
|
|
"helptext": "A valid tier 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
|
|
- in: query
|
|
name: tier
|
|
required: false
|
|
description: The device tier for the pool (hdd, ssd, nvme, or default)
|
|
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),
|
|
reqargs.get("tier", None),
|
|
)
|
|
|
|
|
|
api.add_resource(API_Storage_Ceph_Pool_Root, "/storage/ceph/pool")
|
|
|
|
|
|
# /storage/ceph/pool/<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.",
|
|
},
|
|
{
|
|
"name": "tier",
|
|
"required": False,
|
|
"choices": ("hdd", "ssd", "nvme", "default"),
|
|
"helptext": "A valid tier 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
|
|
- in: query
|
|
name: tier
|
|
required: false
|
|
description: The device tier for the pool (hdd, ssd, nvme, or default)
|
|
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),
|
|
reqargs.get("tier", None),
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "pgs",
|
|
"required": True,
|
|
"helptext": "A placement group count must be specified.",
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def put(self, pool, reqargs):
|
|
"""
|
|
Adjust Ceph pool {pool}'s placement group count
|
|
---
|
|
tags:
|
|
- storage / ceph
|
|
parameters:
|
|
- in: query
|
|
name: pgs
|
|
type: integer
|
|
required: true
|
|
description: The new number of placement groups (PGs) 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_set_pgs(
|
|
pool,
|
|
reqargs.get("pgs", 0),
|
|
)
|
|
|
|
@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/<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 (B implied or with SI suffix k/M/G/T) must be specified.",
|
|
},
|
|
{
|
|
"name": "force",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@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 (B implied) or with a single-character SI suffix (k/M/G/T)
|
|
- in: query
|
|
name: force
|
|
type: boolean
|
|
required: false
|
|
default: false
|
|
description: Force action if volume creation would violate 80% full soft cap on 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_add(
|
|
reqargs.get("pool", None),
|
|
reqargs.get("volume", None),
|
|
reqargs.get("size", None),
|
|
bool(strtobool(reqargs.get("force", "False"))),
|
|
)
|
|
|
|
|
|
api.add_resource(API_Storage_Ceph_Volume_Root, "/storage/ceph/volume")
|
|
|
|
|
|
# /storage/ceph/volume/<pool>/<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.",
|
|
},
|
|
{
|
|
"name": "force",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@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)
|
|
- in: query
|
|
name: force
|
|
type: boolean
|
|
required: false
|
|
default: false
|
|
description: Force action if volume creation would violate 80% full soft cap on 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_add(
|
|
pool, volume, reqargs.get("size", None), reqargs.get("force", False)
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{"name": "new_size"},
|
|
{"name": "new_name"},
|
|
{"name": "force", "required": False},
|
|
]
|
|
)
|
|
@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: new_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
|
|
- in: query
|
|
name: force
|
|
type: boolean
|
|
required: false
|
|
default: false
|
|
description: Force action if new volume size would violate 80% full soft cap on 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
|
|
"""
|
|
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"), reqargs.get("force", False)
|
|
)
|
|
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/<pool>/<volume>"
|
|
)
|
|
|
|
|
|
# /storage/ceph/volume/<pool>/<volume>/scan
|
|
class API_Storage_Ceph_Volume_Element_Scan(Resource):
|
|
@Authenticator
|
|
def post(self, pool, volume):
|
|
"""
|
|
Scan a Ceph volume {volume} in pool {pool} for stats (after import)
|
|
---
|
|
tags:
|
|
- storage / ceph
|
|
parameters:
|
|
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_scan(pool, volume)
|
|
|
|
|
|
api.add_resource(
|
|
API_Storage_Ceph_Volume_Element_Scan, "/storage/ceph/volume/<pool>/<volume>/scan"
|
|
)
|
|
|
|
|
|
# /storage/ceph/volume/<pool>/<volume>/clone
|
|
class API_Storage_Ceph_Volume_Element_Clone(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "new_volume",
|
|
"required": True,
|
|
"helptext": "A new volume name must be specified.",
|
|
},
|
|
{
|
|
"name": "force",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@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
|
|
- in: query
|
|
name: force
|
|
type: boolean
|
|
required: false
|
|
default: false
|
|
description: Force action if clone volume size would violate 80% full soft cap on 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_clone(
|
|
pool, reqargs.get("new_volume", None), volume, reqargs.get("force", None)
|
|
)
|
|
|
|
|
|
api.add_resource(
|
|
API_Storage_Ceph_Volume_Element_Clone, "/storage/ceph/volume/<pool>/<volume>/clone"
|
|
)
|
|
|
|
|
|
# /storage/ceph/volume/<pool>/<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.",
|
|
},
|
|
{
|
|
"name": "file_size",
|
|
"required": False,
|
|
"location": ["args"],
|
|
},
|
|
]
|
|
)
|
|
@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
|
|
- in: query
|
|
name: file_size
|
|
type: integer
|
|
required: false
|
|
description: The size of the image file, in bytes, if {image_format} is not "raw"
|
|
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),
|
|
reqargs.get("file_size", None),
|
|
)
|
|
|
|
|
|
api.add_resource(
|
|
API_Storage_Ceph_Volume_Element_Upload,
|
|
"/storage/ceph/volume/<pool>/<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/<pool>/<volume>/<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/<pool>/<volume>/<snapshot>",
|
|
)
|
|
|
|
|
|
# /storage/ceph/snapshot/<pool>/<volume>/<snapshot>/rollback
|
|
class API_Storage_Ceph_Snapshot_Rollback_Element(Resource):
|
|
@Authenticator
|
|
def post(self, pool, volume, snapshot):
|
|
"""
|
|
Roll back an RBD volume {volume} in pool {pool} to snapshot {snapshot}
|
|
|
|
WARNING: This action cannot be done on an active RBD volume. All IO MUST be stopped first.
|
|
---
|
|
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_rollback(pool, volume, snapshot)
|
|
|
|
|
|
api.add_resource(
|
|
API_Storage_Ceph_Snapshot_Rollback_Element,
|
|
"/storage/ceph/snapshot/<pool>/<volume>/<snapshot>/rollback",
|
|
)
|
|
|
|
|
|
##########################################################
|
|
# 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)
|
|
migration_max_downtime:
|
|
type: integer
|
|
description: The maximum time in milliseconds that a VM can be down for during a live migration; busy VMs may require a larger max_downtime
|
|
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"},
|
|
{"name": "migration_max_downtime"},
|
|
]
|
|
)
|
|
@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)
|
|
- in: query
|
|
name: migration_max_downtime
|
|
type: integer
|
|
required: false
|
|
description: The maximum time in milliseconds that a VM can be down for during a live migration; busy VMs may require a larger max_downtime
|
|
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),
|
|
reqargs.get("migration_max_downtime", None),
|
|
)
|
|
|
|
|
|
api.add_resource(API_Provisioner_Template_System_Root, "/provisioner/template/system")
|
|
|
|
|
|
# /provisioner/template/system/<template>
|
|
class API_Provisioner_Template_System_Element(Resource):
|
|
@Authenticator
|
|
def get(self, template):
|
|
"""
|
|
Return information about system template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/system-template'
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.list_template_system(template, is_fuzzy=False)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"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"},
|
|
{"name": "migration_max_downtime"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, template, reqargs):
|
|
"""
|
|
Create a new system template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
parameters:
|
|
- 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)
|
|
- in: query
|
|
name: migration_max_downtime
|
|
type: integer
|
|
required: false
|
|
description: The maximum time in milliseconds that a VM can be down for during a live migration; busy VMs may require a larger max_downtime
|
|
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(
|
|
template,
|
|
vcpus,
|
|
vram,
|
|
serial,
|
|
vnc,
|
|
vnc_bind,
|
|
reqargs.get("node_limit", None),
|
|
reqargs.get("node_selector", None),
|
|
node_autostart,
|
|
reqargs.get("migration_method", None),
|
|
reqargs.get("migration_max_downtime", None),
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{"name": "vcpus"},
|
|
{"name": "vram"},
|
|
{"name": "serial"},
|
|
{"name": "vnc"},
|
|
{"name": "vnc_bind"},
|
|
{"name": "node_limit"},
|
|
{"name": "node_selector"},
|
|
{"name": "node_autostart"},
|
|
{"name": "migration_method"},
|
|
{"name": "migration_max_downtime"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def put(self, template, reqargs):
|
|
"""
|
|
Modify an existing system template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
parameters:
|
|
- in: query
|
|
name: vcpus
|
|
type: integer
|
|
description: vCPU count for VM
|
|
- in: query
|
|
name: vram
|
|
type: integer
|
|
description: vRAM size in MB for VM
|
|
- in: query
|
|
name: serial
|
|
type: boolean
|
|
description: Whether to enable serial console for VM
|
|
- in: query
|
|
name: vnc
|
|
type: boolean
|
|
description: Whether to enable VNC console for VM
|
|
- in: query
|
|
name: vnc_bind
|
|
type: string
|
|
description: VNC bind address when VNC console is enabled
|
|
- in: query
|
|
name: node_limit
|
|
type: string
|
|
description: CSV list of node(s) to limit VM assignment to
|
|
- in: query
|
|
name: node_selector
|
|
type: string
|
|
description: Selector to use for VM node assignment on migration/move
|
|
- in: query
|
|
name: node_autostart
|
|
type: boolean
|
|
description: Whether to start VM with node ready state (one-time)
|
|
- in: query
|
|
name: migration_method
|
|
type: string
|
|
description: The preferred migration method (live, shutdown, none)
|
|
- in: query
|
|
name: migration_max_downtime
|
|
type: integer
|
|
description: The maximum time in milliseconds that a VM can be down for during a live migration; busy VMs may require a larger max_downtime
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.modify_template_system(
|
|
template,
|
|
reqargs.get("vcpus", None),
|
|
reqargs.get("vram", None),
|
|
reqargs.get("serial", None),
|
|
reqargs.get("vnc", None),
|
|
reqargs.get("vnc_bind"),
|
|
reqargs.get("node_limit", None),
|
|
reqargs.get("node_selector", None),
|
|
reqargs.get("node_autostart", None),
|
|
reqargs.get("migration_method", None),
|
|
reqargs.get("migration_max_downtime", None),
|
|
)
|
|
|
|
@Authenticator
|
|
def delete(self, template):
|
|
"""
|
|
Remove system template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.delete_template_system(template)
|
|
|
|
|
|
api.add_resource(
|
|
API_Provisioner_Template_System_Element, "/provisioner/template/system/<template>"
|
|
)
|
|
|
|
|
|
# /provisioner/template/network
|
|
class API_Provisioner_Template_Network_Root(Resource):
|
|
@RequestParser([{"name": "limit"}])
|
|
@Authenticator
|
|
def get(self, reqargs):
|
|
"""
|
|
Return a list of network templates
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
definitions:
|
|
- schema:
|
|
type: object
|
|
id: network-template-net
|
|
properties:
|
|
id:
|
|
type: integer
|
|
description: Internal provisioner template ID
|
|
network_template:
|
|
type: integer
|
|
description: Internal provisioner network template ID
|
|
vni:
|
|
type: integer
|
|
description: PVC network VNI
|
|
- schema:
|
|
type: object
|
|
id: network-template
|
|
properties:
|
|
id:
|
|
type: integer
|
|
description: Internal provisioner template ID
|
|
name:
|
|
type: string
|
|
description: Template name
|
|
mac_template:
|
|
type: string
|
|
description: MAC address template for VM
|
|
networks:
|
|
type: array
|
|
items:
|
|
$ref: '#/definitions/network-template-net'
|
|
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/network-template'
|
|
"""
|
|
return api_provisioner.list_template_network(reqargs.get("limit", None))
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "name",
|
|
"required": True,
|
|
"helptext": "A template name must be specified.",
|
|
},
|
|
{"name": "mac_template"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, reqargs):
|
|
"""
|
|
Create a new network template
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
parameters:
|
|
- in: query
|
|
name: name
|
|
type: string
|
|
required: true
|
|
description: Template name
|
|
- in: query
|
|
name: mac_template
|
|
type: string
|
|
required: false
|
|
description: MAC address template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_template_network(
|
|
reqargs.get("name", None), reqargs.get("mac_template", None)
|
|
)
|
|
|
|
|
|
api.add_resource(API_Provisioner_Template_Network_Root, "/provisioner/template/network")
|
|
|
|
|
|
# /provisioner/template/network/<template>
|
|
class API_Provisioner_Template_Network_Element(Resource):
|
|
@Authenticator
|
|
def get(self, template):
|
|
"""
|
|
Return information about network template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/network-template'
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.list_template_network(template, is_fuzzy=False)
|
|
|
|
@RequestParser([{"name": "mac_template"}])
|
|
@Authenticator
|
|
def post(self, template, reqargs):
|
|
"""
|
|
Create a new network template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
parameters:
|
|
- in: query
|
|
name: mac_template
|
|
type: string
|
|
required: false
|
|
description: MAC address template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_template_network(
|
|
template, reqargs.get("mac_template", None)
|
|
)
|
|
|
|
@Authenticator
|
|
def delete(self, template):
|
|
"""
|
|
Remove network template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.delete_template_network(template)
|
|
|
|
|
|
api.add_resource(
|
|
API_Provisioner_Template_Network_Element, "/provisioner/template/network/<template>"
|
|
)
|
|
|
|
|
|
# /provisioner/template/network/<template>/net
|
|
class API_Provisioner_Template_Network_Net_Root(Resource):
|
|
@Authenticator
|
|
def get(self, template):
|
|
"""
|
|
Return a list of networks in network template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: list
|
|
items:
|
|
$ref: '#/definitions/network-template-net'
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
templates = api_provisioner.list_template_network(template, is_fuzzy=False)
|
|
if templates:
|
|
return templates["networks"]
|
|
else:
|
|
return {"message": "Template not found."}, 404
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "vni",
|
|
"required": True,
|
|
"helptext": "A valid VNI must be specified.",
|
|
},
|
|
{
|
|
"name": "permit_duplicate",
|
|
"required": False,
|
|
},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, template, reqargs):
|
|
"""
|
|
Create a new network in network template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
parameters:
|
|
- in: query
|
|
name: vni
|
|
type: integer
|
|
required: false
|
|
description: PVC network VNI
|
|
- in: query
|
|
name: permit_duplicate
|
|
type: boolean
|
|
required: false
|
|
description: Bypass checks to permit duplicate VNIs for niche usecases
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_template_network_element(
|
|
template, reqargs.get("vni", None), reqargs.get("permit_duplicate", False)
|
|
)
|
|
|
|
|
|
api.add_resource(
|
|
API_Provisioner_Template_Network_Net_Root,
|
|
"/provisioner/template/network/<template>/net",
|
|
)
|
|
|
|
|
|
# /provisioner/template/network/<template>/net/<vni>
|
|
class API_Provisioner_Template_Network_Net_Element(Resource):
|
|
@Authenticator
|
|
def get(self, template, vni):
|
|
"""
|
|
Return information about network {vni} in network template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/network-template-net'
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
vni_list = api_provisioner.list_template_network(template, is_fuzzy=False)[
|
|
"networks"
|
|
]
|
|
for _vni in vni_list:
|
|
if int(_vni["vni"]) == int(vni):
|
|
return _vni, 200
|
|
abort(404)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "permit_duplicate",
|
|
"required": False,
|
|
}
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, template, vni, reqargs):
|
|
"""
|
|
Create a new network {vni} in network template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
parameters:
|
|
- in: query
|
|
name: permit_duplicate
|
|
type: boolean
|
|
required: false
|
|
description: Bypass checks to permit duplicate VNIs for niche usecases
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_template_network_element(
|
|
template, vni, reqargs.get("permit_duplicate", False)
|
|
)
|
|
|
|
@Authenticator
|
|
def delete(self, template, vni):
|
|
"""
|
|
Remove network {vni} from network template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.delete_template_network_element(template, vni)
|
|
|
|
|
|
api.add_resource(
|
|
API_Provisioner_Template_Network_Net_Element,
|
|
"/provisioner/template/network/<template>/net/<vni>",
|
|
)
|
|
|
|
|
|
# /provisioner/template/storage
|
|
class API_Provisioner_Template_Storage_Root(Resource):
|
|
@RequestParser([{"name": "limit"}])
|
|
@Authenticator
|
|
def get(self, reqargs):
|
|
"""
|
|
Return a list of storage templates
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
definitions:
|
|
- schema:
|
|
type: object
|
|
id: storage-template-disk
|
|
properties:
|
|
id:
|
|
type: integer
|
|
description: Internal provisioner disk ID
|
|
storage_template:
|
|
type: integer
|
|
description: Internal provisioner storage template ID
|
|
pool:
|
|
type: string
|
|
description: Ceph storage pool for disk
|
|
disk_id:
|
|
type: string
|
|
description: Disk identifier
|
|
disk_size_gb:
|
|
type: string
|
|
description: Disk size in GB
|
|
mountpoint:
|
|
type: string
|
|
description: In-VM mountpoint for disk
|
|
filesystem:
|
|
type: string
|
|
description: Filesystem for disk
|
|
filesystem_args:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: Filesystem mkfs arguments
|
|
- schema:
|
|
type: object
|
|
id: storage-template
|
|
properties:
|
|
id:
|
|
type: integer
|
|
description: Internal provisioner template ID
|
|
name:
|
|
type: string
|
|
description: Template name
|
|
disks:
|
|
type: array
|
|
items:
|
|
$ref: '#/definitions/storage-template-disk'
|
|
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/storage-template'
|
|
"""
|
|
return api_provisioner.list_template_storage(reqargs.get("limit", None))
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "name",
|
|
"required": True,
|
|
"helptext": "A template name must be specified.",
|
|
}
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, reqargs):
|
|
"""
|
|
Create a new storage template
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
parameters:
|
|
- in: query
|
|
name: name
|
|
type: string
|
|
required: true
|
|
description: Template name
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_template_storage(reqargs.get("name", None))
|
|
|
|
|
|
api.add_resource(API_Provisioner_Template_Storage_Root, "/provisioner/template/storage")
|
|
|
|
|
|
# /provisioner/template/storage/<template>
|
|
class API_Provisioner_Template_Storage_Element(Resource):
|
|
@Authenticator
|
|
def get(self, template):
|
|
"""
|
|
Return information about storage template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/storage-template'
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.list_template_storage(template, is_fuzzy=False)
|
|
|
|
@Authenticator
|
|
def post(self, template):
|
|
"""
|
|
Create a new storage template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_template_storage(template)
|
|
|
|
@Authenticator
|
|
def delete(self, template):
|
|
"""
|
|
Remove storage template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.delete_template_storage(template)
|
|
|
|
|
|
api.add_resource(
|
|
API_Provisioner_Template_Storage_Element, "/provisioner/template/storage/<template>"
|
|
)
|
|
|
|
|
|
# /provisioner/template/storage/<template>/disk
|
|
class API_Provisioner_Template_Storage_Disk_Root(Resource):
|
|
@RequestParser([{"name": "limit"}])
|
|
@Authenticator
|
|
def get(self, template, reqargs):
|
|
"""
|
|
Return a list of disks in network template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: list
|
|
items:
|
|
$ref: '#/definitions/storage-template-disk'
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
templates = api_provisioner.list_template_storage(template, is_fuzzy=False)
|
|
if templates:
|
|
return templates["disks"]
|
|
else:
|
|
return {"message": "Template not found."}, 404
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"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"},
|
|
{"name": "filesystem_arg", "action": "append"},
|
|
{"name": "mountpoint"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, template, reqargs):
|
|
"""
|
|
Create a new disk in storage template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
parameters:
|
|
- in: query
|
|
name: disk_id
|
|
type: string
|
|
required: true
|
|
description: Disk identifier in "sdX"/"vdX" format (e.g. "sda", "vdb", etc.)
|
|
- in: query
|
|
name: pool
|
|
type: string
|
|
required: true
|
|
description: ceph storage pool for disk
|
|
- in: query
|
|
name: source_volume
|
|
type: string
|
|
required: false
|
|
description: Source storage volume; not compatible with other options
|
|
- in: query
|
|
name: disk_size
|
|
type: integer
|
|
required: false
|
|
description: Disk size in GB; not compatible with source_volume
|
|
- in: query
|
|
name: filesystem
|
|
type: string
|
|
required: false
|
|
description: Filesystem for disk; not compatible with source_volume
|
|
- in: query
|
|
name: filesystem_arg
|
|
type: string
|
|
required: false
|
|
description: Filesystem mkfs argument in "-X=foo" format; may be specified multiple times to add multiple arguments
|
|
- in: query
|
|
name: mountpoint
|
|
type: string
|
|
required: false
|
|
description: In-VM mountpoint for disk; not compatible with source_volume
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_template_storage_element(
|
|
template,
|
|
reqargs.get("disk_id", None),
|
|
reqargs.get("pool", None),
|
|
reqargs.get("source_volume", None),
|
|
reqargs.get("disk_size", None),
|
|
reqargs.get("filesystem", None),
|
|
reqargs.get("filesystem_arg", []),
|
|
reqargs.get("mountpoint", None),
|
|
)
|
|
|
|
|
|
api.add_resource(
|
|
API_Provisioner_Template_Storage_Disk_Root,
|
|
"/provisioner/template/storage/<template>/disk",
|
|
)
|
|
|
|
|
|
# /provisioner/template/storage/<template>/disk/<disk_id>
|
|
class API_Provisioner_Template_Storage_Disk_Element(Resource):
|
|
@Authenticator
|
|
def get(self, template, disk_id):
|
|
"""
|
|
Return information about disk {disk_id} in storage template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/storage-template-disk'
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
disk_list = api_provisioner.list_template_storage(template, is_fuzzy=False)[
|
|
"disks"
|
|
]
|
|
for _disk in disk_list:
|
|
if _disk["disk_id"] == disk_id:
|
|
return _disk, 200
|
|
abort(404)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "pool",
|
|
"required": True,
|
|
"helptext": "A storage pool must be specified.",
|
|
},
|
|
{"name": "source_volume"},
|
|
{"name": "disk_size"},
|
|
{"name": "filesystem"},
|
|
{"name": "filesystem_arg", "action": "append"},
|
|
{"name": "mountpoint"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, template, disk_id, reqargs):
|
|
"""
|
|
Create a new disk {disk_id} in storage template {template}
|
|
Alternative to "POST /provisioner/template/storage/<template>/disk?disk_id=<disk_id>"
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
parameters:
|
|
- in: query
|
|
name: pool
|
|
type: string
|
|
required: true
|
|
description: ceph storage pool for disk
|
|
- in: query
|
|
name: source_volume
|
|
type: string
|
|
required: false
|
|
description: Source storage volume; not compatible with other options
|
|
- in: query
|
|
name: disk_size
|
|
type: integer
|
|
required: false
|
|
description: Disk size in GB; not compatible with source_volume
|
|
- in: query
|
|
name: filesystem
|
|
type: string
|
|
required: false
|
|
description: Filesystem for disk; not compatible with source_volume
|
|
- in: query
|
|
name: filesystem_arg
|
|
type: string
|
|
required: false
|
|
description: Filesystem mkfs argument in "-X=foo" format; may be specified multiple times to add multiple arguments
|
|
- in: query
|
|
name: mountpoint
|
|
type: string
|
|
required: false
|
|
description: In-VM mountpoint for disk; not compatible with source_volume
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_template_storage_element(
|
|
template,
|
|
disk_id,
|
|
reqargs.get("pool", None),
|
|
reqargs.get("source_volume", None),
|
|
reqargs.get("disk_size", None),
|
|
reqargs.get("filesystem", None),
|
|
reqargs.get("filesystem_arg", []),
|
|
reqargs.get("mountpoint", None),
|
|
)
|
|
|
|
@Authenticator
|
|
def delete(self, template, disk_id):
|
|
"""
|
|
Remove disk {disk_id} from storage template {template}
|
|
---
|
|
tags:
|
|
- provisioner / template
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.delete_template_storage_element(template, disk_id)
|
|
|
|
|
|
api.add_resource(
|
|
API_Provisioner_Template_Storage_Disk_Element,
|
|
"/provisioner/template/storage/<template>/disk/<disk_id>",
|
|
)
|
|
|
|
|
|
# /provisioner/userdata
|
|
class API_Provisioner_Userdata_Root(Resource):
|
|
@RequestParser([{"name": "limit"}])
|
|
@Authenticator
|
|
def get(self, reqargs):
|
|
"""
|
|
Return a list of userdata documents
|
|
---
|
|
tags:
|
|
- provisioner
|
|
definitions:
|
|
- schema:
|
|
type: object
|
|
id: userdata
|
|
properties:
|
|
id:
|
|
type: integer
|
|
description: Internal provisioner ID
|
|
name:
|
|
type: string
|
|
description: Userdata name
|
|
userdata:
|
|
type: string
|
|
description: Raw userdata configuration document
|
|
parameters:
|
|
- in: query
|
|
name: limit
|
|
type: string
|
|
required: false
|
|
description: A userdata name search limit; fuzzy by default, use ^/$ to force exact matches
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: list
|
|
items:
|
|
$ref: '#/definitions/userdata'
|
|
"""
|
|
return api_provisioner.list_userdata(reqargs.get("limit", None))
|
|
|
|
@RequestParser(
|
|
[
|
|
{"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):
|
|
"""
|
|
Create a new userdata document
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: name
|
|
type: string
|
|
required: true
|
|
description: Userdata name
|
|
- in: query
|
|
name: data
|
|
type: string
|
|
required: true
|
|
description: Raw userdata configuration document
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_userdata(
|
|
reqargs.get("name", None), reqargs.get("data", None)
|
|
)
|
|
|
|
|
|
api.add_resource(API_Provisioner_Userdata_Root, "/provisioner/userdata")
|
|
|
|
|
|
# /provisioner/userdata/<userdata>
|
|
class API_Provisioner_Userdata_Element(Resource):
|
|
@Authenticator
|
|
def get(self, userdata):
|
|
"""
|
|
Return information about userdata document {userdata}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/userdata'
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.list_userdata(userdata, is_fuzzy=False)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "data",
|
|
"required": True,
|
|
"helptext": "A userdata document must be specified.",
|
|
}
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, userdata, reqargs):
|
|
"""
|
|
Create a new userdata document {userdata}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: data
|
|
type: string
|
|
required: true
|
|
description: Raw userdata configuration document
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_userdata(userdata, reqargs.get("data", None))
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "data",
|
|
"required": True,
|
|
"helptext": "A userdata document must be specified.",
|
|
}
|
|
]
|
|
)
|
|
@Authenticator
|
|
def put(self, userdata, reqargs):
|
|
"""
|
|
Update userdata document {userdata}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: data
|
|
type: string
|
|
required: true
|
|
description: Raw userdata configuration document
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.update_userdata(userdata, reqargs.get("data", None))
|
|
|
|
@Authenticator
|
|
def delete(self, userdata):
|
|
"""
|
|
Remove userdata document {userdata}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.delete_userdata(userdata)
|
|
|
|
|
|
api.add_resource(API_Provisioner_Userdata_Element, "/provisioner/userdata/<userdata>")
|
|
|
|
|
|
# /provisioner/script
|
|
class API_Provisioner_Script_Root(Resource):
|
|
@RequestParser([{"name": "limit"}])
|
|
@Authenticator
|
|
def get(self, reqargs):
|
|
"""
|
|
Return a list of scripts
|
|
---
|
|
tags:
|
|
- provisioner
|
|
definitions:
|
|
- schema:
|
|
type: object
|
|
id: script
|
|
properties:
|
|
id:
|
|
type: integer
|
|
description: Internal provisioner script ID
|
|
name:
|
|
type: string
|
|
description: Script name
|
|
script:
|
|
type: string
|
|
description: Raw Python script document
|
|
parameters:
|
|
- in: query
|
|
name: limit
|
|
type: string
|
|
required: false
|
|
description: A script name search limit; fuzzy by default, use ^/$ to force exact matches
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: list
|
|
items:
|
|
$ref: '#/definitions/script'
|
|
"""
|
|
return api_provisioner.list_script(reqargs.get("limit", None))
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"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):
|
|
"""
|
|
Create a new script
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: name
|
|
type: string
|
|
required: true
|
|
description: Script name
|
|
- in: query
|
|
name: data
|
|
type: string
|
|
required: true
|
|
description: Raw Python script document
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_script(
|
|
reqargs.get("name", None), reqargs.get("data", None)
|
|
)
|
|
|
|
|
|
api.add_resource(API_Provisioner_Script_Root, "/provisioner/script")
|
|
|
|
|
|
# /provisioner/script/<script>
|
|
class API_Provisioner_Script_Element(Resource):
|
|
@Authenticator
|
|
def get(self, script):
|
|
"""
|
|
Return information about script {script}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/script'
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.list_script(script, is_fuzzy=False)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "data",
|
|
"required": True,
|
|
"helptext": "A script document must be specified.",
|
|
}
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, script, reqargs):
|
|
"""
|
|
Create a new script {script}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: data
|
|
type: string
|
|
required: true
|
|
description: Raw Python script document
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_script(script, reqargs.get("data", None))
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "data",
|
|
"required": True,
|
|
"helptext": "A script document must be specified.",
|
|
}
|
|
]
|
|
)
|
|
@Authenticator
|
|
def put(self, script, reqargs):
|
|
"""
|
|
Update script {script}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: data
|
|
type: string
|
|
required: true
|
|
description: Raw Python script document
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.update_script(script, reqargs.get("data", None))
|
|
|
|
@Authenticator
|
|
def delete(self, script):
|
|
"""
|
|
Remove script {script}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.delete_script(script)
|
|
|
|
|
|
api.add_resource(API_Provisioner_Script_Element, "/provisioner/script/<script>")
|
|
|
|
|
|
# /provisioner/profile
|
|
class API_Provisioner_OVA_Root(Resource):
|
|
@RequestParser([{"name": "limit"}])
|
|
@Authenticator
|
|
def get(self, reqargs):
|
|
"""
|
|
Return a list of OVA sources
|
|
---
|
|
tags:
|
|
- provisioner
|
|
definitions:
|
|
- schema:
|
|
type: object
|
|
id: ova
|
|
properties:
|
|
id:
|
|
type: integer
|
|
description: Internal provisioner OVA ID
|
|
name:
|
|
type: string
|
|
description: OVA name
|
|
volumes:
|
|
type: list
|
|
items:
|
|
type: object
|
|
id: ova_volume
|
|
properties:
|
|
disk_id:
|
|
type: string
|
|
description: Disk identifier
|
|
disk_size_gb:
|
|
type: string
|
|
description: Disk size in GB
|
|
pool:
|
|
type: string
|
|
description: Pool containing the OVA volume
|
|
volume_name:
|
|
type: string
|
|
description: Storage volume containing the OVA image
|
|
volume_format:
|
|
type: string
|
|
description: OVA image format
|
|
parameters:
|
|
- in: query
|
|
name: limit
|
|
type: string
|
|
required: false
|
|
description: An OVA name search limit; fuzzy by default, use ^/$ to force exact matches
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: list
|
|
items:
|
|
$ref: '#/definitions/ova'
|
|
"""
|
|
return api_ova.list_ova(reqargs.get("limit", None))
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"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):
|
|
"""
|
|
Upload an OVA image to the cluster
|
|
|
|
The API client is responsible for determining and setting the ova_size value, as this value cannot be determined dynamically before the upload proceeds.
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: pool
|
|
type: string
|
|
required: true
|
|
description: Storage pool name
|
|
- in: query
|
|
name: name
|
|
type: string
|
|
required: true
|
|
description: OVA name on the cluster (usually identical to the OVA file name)
|
|
- in: query
|
|
name: ova_size
|
|
type: string
|
|
required: true
|
|
description: Size of the OVA file in bytes
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_ova.upload_ova(
|
|
reqargs.get("pool", None),
|
|
reqargs.get("name", None),
|
|
reqargs.get("ova_size", None),
|
|
)
|
|
|
|
|
|
api.add_resource(API_Provisioner_OVA_Root, "/provisioner/ova")
|
|
|
|
|
|
# /provisioner/ova/<ova>
|
|
class API_Provisioner_OVA_Element(Resource):
|
|
@Authenticator
|
|
def get(self, ova):
|
|
"""
|
|
Return information about OVA image {ova}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/ova'
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_ova.list_ova(ova, is_fuzzy=False)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"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):
|
|
"""
|
|
Upload an OVA image to the cluster
|
|
|
|
The API client is responsible for determining and setting the ova_size value, as this value cannot be determined dynamically before the upload proceeds.
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: pool
|
|
type: string
|
|
required: true
|
|
description: Storage pool name
|
|
- in: query
|
|
name: ova_size
|
|
type: string
|
|
required: true
|
|
description: Size of the OVA file in bytes
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_ova.upload_ova(
|
|
reqargs.get("pool", None),
|
|
ova,
|
|
reqargs.get("ova_size", None),
|
|
)
|
|
|
|
@Authenticator
|
|
def delete(self, ova):
|
|
"""
|
|
Remove ova {ova}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_ova.delete_ova(ova)
|
|
|
|
|
|
api.add_resource(API_Provisioner_OVA_Element, "/provisioner/ova/<ova>")
|
|
|
|
|
|
# /provisioner/profile
|
|
class API_Provisioner_Profile_Root(Resource):
|
|
@RequestParser([{"name": "limit"}])
|
|
@Authenticator
|
|
def get(self, reqargs):
|
|
"""
|
|
Return a list of profiles
|
|
---
|
|
tags:
|
|
- provisioner
|
|
definitions:
|
|
- schema:
|
|
type: object
|
|
id: profile
|
|
properties:
|
|
id:
|
|
type: integer
|
|
description: Internal provisioner profile ID
|
|
name:
|
|
type: string
|
|
description: Profile name
|
|
script:
|
|
type: string
|
|
description: Script name
|
|
system_template:
|
|
type: string
|
|
description: System template name
|
|
network_template:
|
|
type: string
|
|
description: Network template name
|
|
storage_template:
|
|
type: string
|
|
description: Storage template name
|
|
userdata:
|
|
type: string
|
|
description: Userdata template name
|
|
arguments:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: Script install() function keyword arguments in "arg=data" format
|
|
parameters:
|
|
- in: query
|
|
name: limit
|
|
type: string
|
|
required: false
|
|
description: A profile name search limit; fuzzy by default, use ^/$ to force exact matches
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: list
|
|
items:
|
|
$ref: '#/definitions/profile'
|
|
"""
|
|
return api_provisioner.list_profile(reqargs.get("limit", None))
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"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",
|
|
"required": True,
|
|
"helptext": "A system_template must be specified.",
|
|
},
|
|
{"name": "network_template"},
|
|
{"name": "storage_template"},
|
|
{"name": "userdata"},
|
|
{
|
|
"name": "script",
|
|
"required": True,
|
|
"helptext": "A script must be specified.",
|
|
},
|
|
{"name": "ova"},
|
|
{"name": "arg", "action": "append"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, reqargs):
|
|
"""
|
|
Create a new profile
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: name
|
|
type: string
|
|
required: true
|
|
description: Profile name
|
|
- in: query
|
|
name: profile_type
|
|
type: string
|
|
required: true
|
|
description: Profile type
|
|
enum:
|
|
- provisioner
|
|
- ova
|
|
- in: query
|
|
name: script
|
|
type: string
|
|
required: true
|
|
description: Script name
|
|
- in: query
|
|
name: system_template
|
|
type: string
|
|
required: true
|
|
description: System template name
|
|
- in: query
|
|
name: network_template
|
|
type: string
|
|
required: false
|
|
description: Network template name
|
|
- in: query
|
|
name: storage_template
|
|
type: string
|
|
required: false
|
|
description: Storage template name
|
|
- in: query
|
|
name: userdata
|
|
type: string
|
|
required: false
|
|
description: Userdata template name
|
|
- in: query
|
|
name: ova
|
|
type: string
|
|
required: false
|
|
description: OVA image source
|
|
- in: query
|
|
name: arg
|
|
type: string
|
|
description: Script install() function keywork argument in "arg=data" format; may be specified multiple times to add multiple arguments
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_profile(
|
|
reqargs.get("name", None),
|
|
reqargs.get("profile_type", None),
|
|
reqargs.get("system_template", None),
|
|
reqargs.get("network_template", None),
|
|
reqargs.get("storage_template", None),
|
|
reqargs.get("userdata", None),
|
|
reqargs.get("script", None),
|
|
reqargs.get("ova", None),
|
|
reqargs.get("arg", []),
|
|
)
|
|
|
|
|
|
api.add_resource(API_Provisioner_Profile_Root, "/provisioner/profile")
|
|
|
|
|
|
# /provisioner/profile/<profile>
|
|
class API_Provisioner_Profile_Element(Resource):
|
|
@Authenticator
|
|
def get(self, profile):
|
|
"""
|
|
Return information about profile {profile}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
$ref: '#/definitions/profile'
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.list_profile(profile, is_fuzzy=False)
|
|
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"name": "profile_type",
|
|
"required": True,
|
|
"helptext": "A profile type must be specified.",
|
|
},
|
|
{
|
|
"name": "system_template",
|
|
"required": True,
|
|
"helptext": "A system_template must be specified.",
|
|
},
|
|
{"name": "network_template"},
|
|
{"name": "storage_template"},
|
|
{"name": "userdata"},
|
|
{
|
|
"name": "script",
|
|
"required": True,
|
|
"helptext": "A script must be specified.",
|
|
},
|
|
{"name": "ova"},
|
|
{"name": "arg", "action": "append"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, profile, reqargs):
|
|
"""
|
|
Create a new profile {profile}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: profile_type
|
|
type: string
|
|
required: true
|
|
description: Profile type
|
|
enum:
|
|
- provisioner
|
|
- ova
|
|
- in: query
|
|
name: script
|
|
type: string
|
|
required: true
|
|
description: Script name
|
|
- in: query
|
|
name: system_template
|
|
type: string
|
|
required: true
|
|
description: System template name
|
|
- in: query
|
|
name: network_template
|
|
type: string
|
|
required: false
|
|
description: Network template name
|
|
- in: query
|
|
name: storage_template
|
|
type: string
|
|
required: false
|
|
description: Storage template name
|
|
- in: query
|
|
name: userdata
|
|
type: string
|
|
required: false
|
|
description: Userdata template name
|
|
- in: query
|
|
name: ova
|
|
type: string
|
|
required: false
|
|
description: OVA image source
|
|
- in: query
|
|
name: arg
|
|
type: string
|
|
description: Script install() function keywork argument in "arg=data" format; may be specified multiple times to add multiple arguments
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.create_profile(
|
|
profile,
|
|
reqargs.get("profile_type", None),
|
|
reqargs.get("system_template", None),
|
|
reqargs.get("network_template", None),
|
|
reqargs.get("storage_template", None),
|
|
reqargs.get("userdata", None),
|
|
reqargs.get("script", None),
|
|
reqargs.get("ova", None),
|
|
reqargs.get("arg", []),
|
|
)
|
|
|
|
@RequestParser(
|
|
[
|
|
{"name": "system_template"},
|
|
{"name": "network_template"},
|
|
{"name": "storage_template"},
|
|
{"name": "userdata"},
|
|
{"name": "script"},
|
|
{"name": "arg", "action": "append"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def put(self, profile, reqargs):
|
|
"""
|
|
Modify profile {profile}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: script
|
|
type: string
|
|
required: false
|
|
description: Script name
|
|
- in: query
|
|
name: system_template
|
|
type: string
|
|
required: false
|
|
description: System template name
|
|
- in: query
|
|
name: network_template
|
|
type: string
|
|
required: false
|
|
description: Network template name
|
|
- in: query
|
|
name: storage_template
|
|
type: string
|
|
required: false
|
|
description: Storage template name
|
|
- in: query
|
|
name: userdata
|
|
type: string
|
|
required: false
|
|
description: Userdata template name
|
|
- in: query
|
|
name: arg
|
|
type: string
|
|
description: Script install() function keywork argument in "arg=data" format; may be specified multiple times to add multiple arguments
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.modify_profile(
|
|
profile,
|
|
None, # Can't modify the profile type
|
|
reqargs.get("system_template", None),
|
|
reqargs.get("network_template", None),
|
|
reqargs.get("storage_template", None),
|
|
reqargs.get("userdata", None),
|
|
reqargs.get("script", None),
|
|
None, # Can't modify the OVA
|
|
reqargs.get("arg", []),
|
|
)
|
|
|
|
@Authenticator
|
|
def delete(self, profile):
|
|
"""
|
|
Remove profile {profile}
|
|
---
|
|
tags:
|
|
- provisioner
|
|
responses:
|
|
200:
|
|
description: OK
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
404:
|
|
description: Not found
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
return api_provisioner.delete_profile(profile)
|
|
|
|
|
|
api.add_resource(API_Provisioner_Profile_Element, "/provisioner/profile/<profile>")
|
|
|
|
|
|
# /provisioner/create
|
|
class API_Provisioner_Create_Root(Resource):
|
|
@RequestParser(
|
|
[
|
|
{
|
|
"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"},
|
|
]
|
|
)
|
|
@Authenticator
|
|
def post(self, reqargs):
|
|
"""
|
|
Create a new virtual machine
|
|
Note: Starts a background job in the pvc-provisioner-worker Celery worker while returning a task ID; the task ID can be used to query the "GET /provisioner/status/<task_id>" endpoint for the job status
|
|
---
|
|
tags:
|
|
- provisioner
|
|
parameters:
|
|
- in: query
|
|
name: name
|
|
type: string
|
|
required: true
|
|
description: Virtual machine name
|
|
- in: query
|
|
name: profile
|
|
type: string
|
|
required: true
|
|
description: Profile name
|
|
- in: query
|
|
name: define_vm
|
|
type: boolean
|
|
required: false
|
|
description: Whether to define the VM on the cluster during provisioning
|
|
- in: query
|
|
name: start_vm
|
|
type: boolean
|
|
required: false
|
|
description: Whether to start the VM after provisioning
|
|
- in: query
|
|
name: arg
|
|
type: string
|
|
description: Script install() function keywork argument in "arg=data" format; may be specified multiple times to add multiple arguments
|
|
responses:
|
|
202:
|
|
description: Accepted
|
|
schema:
|
|
type: object
|
|
description: The Celery job information of the task
|
|
id: CeleryTask
|
|
400:
|
|
description: Bad request
|
|
schema:
|
|
type: object
|
|
id: Message
|
|
"""
|
|
# Verify that the profile is valid
|
|
_list, code = api_provisioner.list_profile(
|
|
reqargs.get("profile", None), is_fuzzy=False
|
|
)
|
|
if code != 200:
|
|
return {
|
|
"message": 'Profile "{}" is not valid.'.format(reqargs.get("profile"))
|
|
}, 400
|
|
|
|
if bool(strtobool(reqargs.get("define_vm", "true"))):
|
|
define_vm = True
|
|
else:
|
|
define_vm = False
|
|
|
|
if bool(strtobool(reqargs.get("start_vm", "true"))):
|
|
start_vm = True
|
|
else:
|
|
start_vm = False
|
|
|
|
task = run_celery_task(
|
|
"provisioner.create",
|
|
vm_name=reqargs.get("name", None),
|
|
profile_name=reqargs.get("profile", None),
|
|
define_vm=define_vm,
|
|
start_vm=start_vm,
|
|
script_run_args=reqargs.get("arg", []),
|
|
run_on="primary",
|
|
)
|
|
return (
|
|
{
|
|
"task_id": task.id,
|
|
"task_name": "provisioner.create",
|
|
"run_on": f"{get_primary_node()} (primary)",
|
|
},
|
|
202,
|
|
{"Location": Api.url_for(api, API_Tasks_Element, task_id=task.id)},
|
|
)
|
|
|
|
|
|
api.add_resource(API_Provisioner_Create_Root, "/provisioner/create")
|