2607 lines
89 KiB
Python
2607 lines
89 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# ceph.py - PVC CLI client function library, Ceph cluster functions
|
|
# Part of the Parallel Virtual Cluster (PVC) system
|
|
#
|
|
# Copyright (C) 2018-2022 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 math
|
|
|
|
from json import dumps, loads
|
|
from requests_toolbelt.multipart.encoder import (
|
|
MultipartEncoder,
|
|
MultipartEncoderMonitor,
|
|
)
|
|
|
|
import pvc.cli_lib.ansiprint as ansiprint
|
|
from pvc.cli_lib.common import UploadProgressBar, call_api
|
|
|
|
#
|
|
# Supplemental functions
|
|
#
|
|
|
|
# Matrix of human-to-byte values
|
|
byte_unit_matrix = {
|
|
"B": 1,
|
|
"K": 1024,
|
|
"M": 1024 * 1024,
|
|
"G": 1024 * 1024 * 1024,
|
|
"T": 1024 * 1024 * 1024 * 1024,
|
|
"P": 1024 * 1024 * 1024 * 1024 * 1024,
|
|
}
|
|
|
|
# Matrix of human-to-metric values
|
|
ops_unit_matrix = {
|
|
"": 1,
|
|
"K": 1000,
|
|
"M": 1000 * 1000,
|
|
"G": 1000 * 1000 * 1000,
|
|
"T": 1000 * 1000 * 1000 * 1000,
|
|
"P": 1000 * 1000 * 1000 * 1000 * 1000,
|
|
}
|
|
|
|
|
|
# Format byte sizes to/from human-readable units
|
|
def format_bytes_tohuman(databytes):
|
|
datahuman = ""
|
|
for unit in sorted(byte_unit_matrix, key=byte_unit_matrix.get, reverse=True):
|
|
new_bytes = int(math.ceil(databytes / byte_unit_matrix[unit]))
|
|
# Round up if 5 or more digits
|
|
if new_bytes > 9999:
|
|
# We can jump down another level
|
|
continue
|
|
else:
|
|
# We're at the end, display with this size
|
|
datahuman = "{}{}".format(new_bytes, unit)
|
|
|
|
return datahuman
|
|
|
|
|
|
def format_bytes_fromhuman(datahuman):
|
|
# Trim off human-readable character
|
|
dataunit = datahuman[-1]
|
|
datasize = int(datahuman[:-1])
|
|
databytes = datasize * byte_unit_matrix[dataunit]
|
|
return "{}B".format(databytes)
|
|
|
|
|
|
# Format ops sizes to/from human-readable units
|
|
def format_ops_tohuman(dataops):
|
|
datahuman = ""
|
|
for unit in sorted(ops_unit_matrix, key=ops_unit_matrix.get, reverse=True):
|
|
new_ops = int(math.ceil(dataops / ops_unit_matrix[unit]))
|
|
# Round up if 6 or more digits
|
|
if new_ops > 99999:
|
|
# We can jump down another level
|
|
continue
|
|
else:
|
|
# We're at the end, display with this size
|
|
datahuman = "{}{}".format(new_ops, unit)
|
|
|
|
return datahuman
|
|
|
|
|
|
def format_ops_fromhuman(datahuman):
|
|
# Trim off human-readable character
|
|
dataunit = datahuman[-1]
|
|
datasize = int(datahuman[:-1])
|
|
dataops = datasize * ops_unit_matrix[dataunit]
|
|
return "{}".format(dataops)
|
|
|
|
|
|
def format_pct_tohuman(datapct):
|
|
datahuman = "{0:.1f}".format(float(datapct * 100.0))
|
|
return datahuman
|
|
|
|
|
|
#
|
|
# Status functions
|
|
#
|
|
def ceph_status(config):
|
|
"""
|
|
Get status of the Ceph cluster
|
|
|
|
API endpoint: GET /api/v1/storage/ceph/status
|
|
API arguments:
|
|
API schema: {json_data_object}
|
|
"""
|
|
response = call_api(config, "get", "/storage/ceph/status")
|
|
|
|
if response.status_code == 200:
|
|
return True, response.json()
|
|
else:
|
|
return False, response.json().get("message", "")
|
|
|
|
|
|
def ceph_util(config):
|
|
"""
|
|
Get utilization of the Ceph cluster
|
|
|
|
API endpoint: GET /api/v1/storage/ceph/utilization
|
|
API arguments:
|
|
API schema: {json_data_object}
|
|
"""
|
|
response = call_api(config, "get", "/storage/ceph/utilization")
|
|
|
|
if response.status_code == 200:
|
|
return True, response.json()
|
|
else:
|
|
return False, response.json().get("message", "")
|
|
|
|
|
|
def format_raw_output(status_data):
|
|
ainformation = list()
|
|
ainformation.append(
|
|
"{bold}Ceph cluster {stype} (primary node {end}{blue}{primary}{end}{bold}){end}\n".format(
|
|
bold=ansiprint.bold(),
|
|
end=ansiprint.end(),
|
|
blue=ansiprint.blue(),
|
|
stype=status_data["type"],
|
|
primary=status_data["primary_node"],
|
|
)
|
|
)
|
|
ainformation.append(status_data["ceph_data"])
|
|
ainformation.append("")
|
|
|
|
return "\n".join(ainformation)
|
|
|
|
|
|
#
|
|
# OSD DB VG functions
|
|
#
|
|
def ceph_osd_db_vg_add(config, node, device):
|
|
"""
|
|
Add new Ceph OSD database volume group
|
|
|
|
API endpoint: POST /api/v1/storage/ceph/osddb
|
|
API arguments: node={node}, device={device}
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {"node": node, "device": device}
|
|
response = call_api(config, "post", "/storage/ceph/osddb", params=params)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
#
|
|
# OSD functions
|
|
#
|
|
def ceph_osd_info(config, osd):
|
|
"""
|
|
Get information about Ceph OSD
|
|
|
|
API endpoint: GET /api/v1/storage/ceph/osd/{osd}
|
|
API arguments:
|
|
API schema: {json_data_object}
|
|
"""
|
|
response = call_api(config, "get", "/storage/ceph/osd/{osd}".format(osd=osd))
|
|
|
|
if response.status_code == 200:
|
|
if isinstance(response.json(), list) and len(response.json()) != 1:
|
|
# No exact match; return not found
|
|
return False, "OSD not found."
|
|
else:
|
|
# Return a single instance if the response is a list
|
|
if isinstance(response.json(), list):
|
|
return True, response.json()[0]
|
|
# This shouldn't happen, but is here just in case
|
|
else:
|
|
return True, response.json()
|
|
else:
|
|
return False, response.json().get("message", "")
|
|
|
|
|
|
def ceph_osd_list(config, limit):
|
|
"""
|
|
Get list information about Ceph OSDs (limited by {limit})
|
|
|
|
API endpoint: GET /api/v1/storage/ceph/osd
|
|
API arguments: limit={limit}
|
|
API schema: [{json_data_object},{json_data_object},etc.]
|
|
"""
|
|
params = dict()
|
|
if limit:
|
|
params["limit"] = limit
|
|
|
|
response = call_api(config, "get", "/storage/ceph/osd", params=params)
|
|
|
|
if response.status_code == 200:
|
|
return True, response.json()
|
|
else:
|
|
return False, response.json().get("message", "")
|
|
|
|
|
|
def ceph_osd_add(config, node, device, weight, ext_db_flag, ext_db_ratio):
|
|
"""
|
|
Add new Ceph OSD
|
|
|
|
API endpoint: POST /api/v1/storage/ceph/osd
|
|
API arguments: node={node}, device={device}, weight={weight}, ext_db={ext_db_flag}, ext_db_ratio={ext_db_ratio}
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {
|
|
"node": node,
|
|
"device": device,
|
|
"weight": weight,
|
|
"ext_db": ext_db_flag,
|
|
"ext_db_ratio": ext_db_ratio,
|
|
}
|
|
response = call_api(config, "post", "/storage/ceph/osd", params=params)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_osd_replace(config, osdid, device, weight):
|
|
"""
|
|
Replace an existing Ceph OSD with a new device
|
|
|
|
API endpoint: POST /api/v1/storage/ceph/osd/{osdid}
|
|
API arguments: device={device}, weight={weight}
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {"device": device, "weight": weight, "yes-i-really-mean-it": "yes"}
|
|
response = call_api(config, "post", f"/storage/ceph/osd/{osdid}", params=params)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_osd_refresh(config, osdid, device):
|
|
"""
|
|
Refresh (reimport) an existing Ceph OSD with device {device}
|
|
|
|
API endpoint: PUT /api/v1/storage/ceph/osd/{osdid}
|
|
API arguments: device={device}
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {
|
|
"device": device,
|
|
}
|
|
response = call_api(config, "put", f"/storage/ceph/osd/{osdid}", params=params)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_osd_remove(config, osdid, force_flag):
|
|
"""
|
|
Remove Ceph OSD
|
|
|
|
API endpoint: DELETE /api/v1/storage/ceph/osd/{osdid}
|
|
API arguments:
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {"force": force_flag, "yes-i-really-mean-it": "yes"}
|
|
response = call_api(
|
|
config, "delete", "/storage/ceph/osd/{osdid}".format(osdid=osdid), params=params
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_osd_state(config, osdid, state):
|
|
"""
|
|
Set state of Ceph OSD
|
|
|
|
API endpoint: POST /api/v1/storage/ceph/osd/{osdid}/state
|
|
API arguments: state={state}
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {"state": state}
|
|
response = call_api(
|
|
config,
|
|
"post",
|
|
"/storage/ceph/osd/{osdid}/state".format(osdid=osdid),
|
|
params=params,
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_osd_option(config, option, action):
|
|
"""
|
|
Set cluster option of Ceph OSDs
|
|
|
|
API endpoint: POST /api/v1/storage/ceph/option
|
|
API arguments: option={option}, action={action}
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {"option": option, "action": action}
|
|
response = call_api(config, "post", "/storage/ceph/option", params=params)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def getOutputColoursOSD(osd_information):
|
|
# Set the UP status
|
|
if osd_information["stats"]["up"] == 1:
|
|
osd_up_flag = "Yes"
|
|
osd_up_colour = ansiprint.green()
|
|
else:
|
|
osd_up_flag = "No"
|
|
osd_up_colour = ansiprint.red()
|
|
|
|
# Set the IN status
|
|
if osd_information["stats"]["in"] == 1:
|
|
osd_in_flag = "Yes"
|
|
osd_in_colour = ansiprint.green()
|
|
else:
|
|
osd_in_flag = "No"
|
|
osd_in_colour = ansiprint.red()
|
|
|
|
return osd_up_flag, osd_up_colour, osd_in_flag, osd_in_colour
|
|
|
|
|
|
def format_list_osd(osd_list):
|
|
# Handle empty list
|
|
if not osd_list:
|
|
osd_list = list()
|
|
|
|
osd_list_output = []
|
|
|
|
osd_id_length = 3
|
|
osd_node_length = 5
|
|
osd_device_length = 6
|
|
osd_db_device_length = 9
|
|
osd_up_length = 4
|
|
osd_in_length = 4
|
|
osd_size_length = 5
|
|
osd_weight_length = 3
|
|
osd_reweight_length = 5
|
|
osd_pgs_length = 4
|
|
osd_used_length = 5
|
|
osd_free_length = 6
|
|
osd_util_length = 6
|
|
osd_var_length = 5
|
|
osd_wrops_length = 4
|
|
osd_wrdata_length = 5
|
|
osd_rdops_length = 4
|
|
osd_rddata_length = 5
|
|
|
|
for osd_information in osd_list:
|
|
try:
|
|
# If this happens, the node hasn't checked in fully yet, so use some dummy data
|
|
if osd_information["stats"]["node"] == "|":
|
|
for key in osd_information["stats"].keys():
|
|
if (
|
|
osd_information["stats"][key] == "|"
|
|
or osd_information["stats"][key] is None
|
|
):
|
|
osd_information["stats"][key] = "N/A"
|
|
for key in osd_information.keys():
|
|
if osd_information[key] is None:
|
|
osd_information[key] = "N/A"
|
|
else:
|
|
for key in osd_information["stats"].keys():
|
|
if key in ["utilization", "var"] and isinstance(
|
|
osd_information["stats"][key], float
|
|
):
|
|
osd_information["stats"][key] = round(
|
|
osd_information["stats"][key], 2
|
|
)
|
|
except KeyError:
|
|
print(
|
|
f"Details for OSD {osd_information['id']} missing required keys, skipping."
|
|
)
|
|
continue
|
|
|
|
# Deal with the size to human readable
|
|
osd_information["stats"]["size"] = osd_information["stats"]["kb"] * 1024
|
|
for datatype in "size", "wr_data", "rd_data":
|
|
databytes = osd_information["stats"][datatype]
|
|
if isinstance(databytes, int):
|
|
databytes_formatted = format_bytes_tohuman(databytes)
|
|
else:
|
|
databytes_formatted = databytes
|
|
osd_information["stats"][datatype] = databytes_formatted
|
|
for datatype in "wr_ops", "rd_ops":
|
|
dataops = osd_information["stats"][datatype]
|
|
if isinstance(dataops, int):
|
|
dataops_formatted = format_ops_tohuman(dataops)
|
|
else:
|
|
dataops_formatted = dataops
|
|
osd_information["stats"][datatype] = dataops_formatted
|
|
|
|
# Set the OSD ID length
|
|
_osd_id_length = len(osd_information["id"]) + 1
|
|
if _osd_id_length > osd_id_length:
|
|
osd_id_length = _osd_id_length
|
|
|
|
# Set the OSD node length
|
|
_osd_node_length = len(osd_information["node"]) + 1
|
|
if _osd_node_length > osd_node_length:
|
|
osd_node_length = _osd_node_length
|
|
|
|
# Set the OSD device length
|
|
_osd_device_length = len(osd_information["device"]) + 1
|
|
if _osd_device_length > osd_device_length:
|
|
osd_device_length = _osd_device_length
|
|
|
|
# Set the OSD db_device length
|
|
_osd_db_device_length = len(osd_information["db_device"]) + 1
|
|
if _osd_db_device_length > osd_db_device_length:
|
|
osd_db_device_length = _osd_db_device_length
|
|
|
|
# Set the size and length
|
|
_osd_size_length = len(str(osd_information["stats"]["size"])) + 1
|
|
if _osd_size_length > osd_size_length:
|
|
osd_size_length = _osd_size_length
|
|
|
|
# Set the weight and length
|
|
_osd_weight_length = len(str(osd_information["stats"]["weight"])) + 1
|
|
if _osd_weight_length > osd_weight_length:
|
|
osd_weight_length = _osd_weight_length
|
|
|
|
# Set the reweight and length
|
|
_osd_reweight_length = len(str(osd_information["stats"]["reweight"])) + 1
|
|
if _osd_reweight_length > osd_reweight_length:
|
|
osd_reweight_length = _osd_reweight_length
|
|
|
|
# Set the pgs and length
|
|
_osd_pgs_length = len(str(osd_information["stats"]["pgs"])) + 1
|
|
if _osd_pgs_length > osd_pgs_length:
|
|
osd_pgs_length = _osd_pgs_length
|
|
|
|
# Set the used/available/utlization%/variance and lengths
|
|
_osd_used_length = len(osd_information["stats"]["used"]) + 1
|
|
if _osd_used_length > osd_used_length:
|
|
osd_used_length = _osd_used_length
|
|
|
|
_osd_free_length = len(osd_information["stats"]["avail"]) + 1
|
|
if _osd_free_length > osd_free_length:
|
|
osd_free_length = _osd_free_length
|
|
|
|
_osd_util_length = len(str(osd_information["stats"]["utilization"])) + 1
|
|
if _osd_util_length > osd_util_length:
|
|
osd_util_length = _osd_util_length
|
|
|
|
_osd_var_length = len(str(osd_information["stats"]["var"])) + 1
|
|
if _osd_var_length > osd_var_length:
|
|
osd_var_length = _osd_var_length
|
|
|
|
# Set the read/write IOPS/data and length
|
|
_osd_wrops_length = len(osd_information["stats"]["wr_ops"]) + 1
|
|
if _osd_wrops_length > osd_wrops_length:
|
|
osd_wrops_length = _osd_wrops_length
|
|
|
|
_osd_wrdata_length = len(osd_information["stats"]["wr_data"]) + 1
|
|
if _osd_wrdata_length > osd_wrdata_length:
|
|
osd_wrdata_length = _osd_wrdata_length
|
|
|
|
_osd_rdops_length = len(osd_information["stats"]["rd_ops"]) + 1
|
|
if _osd_rdops_length > osd_rdops_length:
|
|
osd_rdops_length = _osd_rdops_length
|
|
|
|
_osd_rddata_length = len(osd_information["stats"]["rd_data"]) + 1
|
|
if _osd_rddata_length > osd_rddata_length:
|
|
osd_rddata_length = _osd_rddata_length
|
|
|
|
# Format the output header
|
|
osd_list_output.append(
|
|
"{bold}{osd_header: <{osd_header_length}} {state_header: <{state_header_length}} {details_header: <{details_header_length}} {read_header: <{read_header_length}} {write_header: <{write_header_length}}{end_bold}".format(
|
|
bold=ansiprint.bold(),
|
|
end_bold=ansiprint.end(),
|
|
osd_header_length=osd_id_length
|
|
+ osd_node_length
|
|
+ osd_device_length
|
|
+ osd_db_device_length
|
|
+ 3,
|
|
state_header_length=osd_up_length + osd_in_length + 1,
|
|
details_header_length=osd_size_length
|
|
+ osd_pgs_length
|
|
+ osd_weight_length
|
|
+ osd_reweight_length
|
|
+ osd_used_length
|
|
+ osd_free_length
|
|
+ osd_util_length
|
|
+ osd_var_length
|
|
+ 7,
|
|
read_header_length=osd_rdops_length + osd_rddata_length + 1,
|
|
write_header_length=osd_wrops_length + osd_wrdata_length + 1,
|
|
osd_header="OSDs "
|
|
+ "".join(
|
|
[
|
|
"-"
|
|
for _ in range(
|
|
5,
|
|
osd_id_length
|
|
+ osd_node_length
|
|
+ osd_device_length
|
|
+ osd_db_device_length
|
|
+ 2,
|
|
)
|
|
]
|
|
),
|
|
state_header="State "
|
|
+ "".join(["-" for _ in range(6, osd_up_length + osd_in_length)]),
|
|
details_header="Details "
|
|
+ "".join(
|
|
[
|
|
"-"
|
|
for _ in range(
|
|
8,
|
|
osd_size_length
|
|
+ osd_pgs_length
|
|
+ osd_weight_length
|
|
+ osd_reweight_length
|
|
+ osd_used_length
|
|
+ osd_free_length
|
|
+ osd_util_length
|
|
+ osd_var_length
|
|
+ 6,
|
|
)
|
|
]
|
|
),
|
|
read_header="Read "
|
|
+ "".join(["-" for _ in range(5, osd_rdops_length + osd_rddata_length)]),
|
|
write_header="Write "
|
|
+ "".join(["-" for _ in range(6, osd_wrops_length + osd_wrdata_length)]),
|
|
)
|
|
)
|
|
|
|
osd_list_output.append(
|
|
"{bold}\
|
|
{osd_id: <{osd_id_length}} \
|
|
{osd_node: <{osd_node_length}} \
|
|
{osd_device: <{osd_device_length}} \
|
|
{osd_db_device: <{osd_db_device_length}} \
|
|
{osd_up: <{osd_up_length}} \
|
|
{osd_in: <{osd_in_length}} \
|
|
{osd_size: <{osd_size_length}} \
|
|
{osd_pgs: <{osd_pgs_length}} \
|
|
{osd_weight: <{osd_weight_length}} \
|
|
{osd_reweight: <{osd_reweight_length}} \
|
|
{osd_used: <{osd_used_length}} \
|
|
{osd_free: <{osd_free_length}} \
|
|
{osd_util: <{osd_util_length}} \
|
|
{osd_var: <{osd_var_length}} \
|
|
{osd_rdops: <{osd_rdops_length}} \
|
|
{osd_rddata: <{osd_rddata_length}} \
|
|
{osd_wrops: <{osd_wrops_length}} \
|
|
{osd_wrdata: <{osd_wrdata_length}} \
|
|
{end_bold}".format(
|
|
bold=ansiprint.bold(),
|
|
end_bold=ansiprint.end(),
|
|
osd_id_length=osd_id_length,
|
|
osd_node_length=osd_node_length,
|
|
osd_device_length=osd_device_length,
|
|
osd_db_device_length=osd_db_device_length,
|
|
osd_up_length=osd_up_length,
|
|
osd_in_length=osd_in_length,
|
|
osd_size_length=osd_size_length,
|
|
osd_pgs_length=osd_pgs_length,
|
|
osd_weight_length=osd_weight_length,
|
|
osd_reweight_length=osd_reweight_length,
|
|
osd_used_length=osd_used_length,
|
|
osd_free_length=osd_free_length,
|
|
osd_util_length=osd_util_length,
|
|
osd_var_length=osd_var_length,
|
|
osd_wrops_length=osd_wrops_length,
|
|
osd_wrdata_length=osd_wrdata_length,
|
|
osd_rdops_length=osd_rdops_length,
|
|
osd_rddata_length=osd_rddata_length,
|
|
osd_id="ID",
|
|
osd_node="Node",
|
|
osd_device="Block",
|
|
osd_db_device="DB Block",
|
|
osd_up="Up",
|
|
osd_in="In",
|
|
osd_size="Size",
|
|
osd_pgs="PGs",
|
|
osd_weight="Wt",
|
|
osd_reweight="ReWt",
|
|
osd_used="Used",
|
|
osd_free="Free",
|
|
osd_util="Util%",
|
|
osd_var="Var",
|
|
osd_wrops="OPS",
|
|
osd_wrdata="Data",
|
|
osd_rdops="OPS",
|
|
osd_rddata="Data",
|
|
)
|
|
)
|
|
|
|
for osd_information in sorted(osd_list, key=lambda x: int(x["id"])):
|
|
osd_up_flag, osd_up_colour, osd_in_flag, osd_in_colour = getOutputColoursOSD(
|
|
osd_information
|
|
)
|
|
|
|
osd_db_device = osd_information["db_device"]
|
|
if not osd_db_device:
|
|
osd_db_device = "N/A"
|
|
|
|
# Format the output header
|
|
osd_list_output.append(
|
|
"{bold}\
|
|
{osd_id: <{osd_id_length}} \
|
|
{osd_node: <{osd_node_length}} \
|
|
{osd_device: <{osd_device_length}} \
|
|
{osd_db_device: <{osd_db_device_length}} \
|
|
{osd_up_colour}{osd_up: <{osd_up_length}}{end_colour} \
|
|
{osd_in_colour}{osd_in: <{osd_in_length}}{end_colour} \
|
|
{osd_size: <{osd_size_length}} \
|
|
{osd_pgs: <{osd_pgs_length}} \
|
|
{osd_weight: <{osd_weight_length}} \
|
|
{osd_reweight: <{osd_reweight_length}} \
|
|
{osd_used: <{osd_used_length}} \
|
|
{osd_free: <{osd_free_length}} \
|
|
{osd_util: <{osd_util_length}} \
|
|
{osd_var: <{osd_var_length}} \
|
|
{osd_rdops: <{osd_rdops_length}} \
|
|
{osd_rddata: <{osd_rddata_length}} \
|
|
{osd_wrops: <{osd_wrops_length}} \
|
|
{osd_wrdata: <{osd_wrdata_length}} \
|
|
{end_bold}".format(
|
|
bold="",
|
|
end_bold="",
|
|
end_colour=ansiprint.end(),
|
|
osd_id_length=osd_id_length,
|
|
osd_node_length=osd_node_length,
|
|
osd_device_length=osd_device_length,
|
|
osd_db_device_length=osd_db_device_length,
|
|
osd_up_length=osd_up_length,
|
|
osd_in_length=osd_in_length,
|
|
osd_size_length=osd_size_length,
|
|
osd_pgs_length=osd_pgs_length,
|
|
osd_weight_length=osd_weight_length,
|
|
osd_reweight_length=osd_reweight_length,
|
|
osd_used_length=osd_used_length,
|
|
osd_free_length=osd_free_length,
|
|
osd_util_length=osd_util_length,
|
|
osd_var_length=osd_var_length,
|
|
osd_wrops_length=osd_wrops_length,
|
|
osd_wrdata_length=osd_wrdata_length,
|
|
osd_rdops_length=osd_rdops_length,
|
|
osd_rddata_length=osd_rddata_length,
|
|
osd_id=osd_information["id"],
|
|
osd_node=osd_information["node"],
|
|
osd_device=osd_information["device"],
|
|
osd_db_device=osd_db_device,
|
|
osd_up_colour=osd_up_colour,
|
|
osd_up=osd_up_flag,
|
|
osd_in_colour=osd_in_colour,
|
|
osd_in=osd_in_flag,
|
|
osd_size=osd_information["stats"]["size"],
|
|
osd_pgs=osd_information["stats"]["pgs"],
|
|
osd_weight=osd_information["stats"]["weight"],
|
|
osd_reweight=osd_information["stats"]["reweight"],
|
|
osd_used=osd_information["stats"]["used"],
|
|
osd_free=osd_information["stats"]["avail"],
|
|
osd_util=osd_information["stats"]["utilization"],
|
|
osd_var=osd_information["stats"]["var"],
|
|
osd_wrops=osd_information["stats"]["wr_ops"],
|
|
osd_wrdata=osd_information["stats"]["wr_data"],
|
|
osd_rdops=osd_information["stats"]["rd_ops"],
|
|
osd_rddata=osd_information["stats"]["rd_data"],
|
|
)
|
|
)
|
|
|
|
return "\n".join(osd_list_output)
|
|
|
|
|
|
#
|
|
# Pool functions
|
|
#
|
|
def ceph_pool_info(config, pool):
|
|
"""
|
|
Get information about Ceph OSD
|
|
|
|
API endpoint: GET /api/v1/storage/ceph/pool/{pool}
|
|
API arguments:
|
|
API schema: {json_data_object}
|
|
"""
|
|
response = call_api(config, "get", "/storage/ceph/pool/{pool}".format(pool=pool))
|
|
|
|
if response.status_code == 200:
|
|
if isinstance(response.json(), list) and len(response.json()) != 1:
|
|
# No exact match; return not found
|
|
return False, "Pool not found."
|
|
else:
|
|
# Return a single instance if the response is a list
|
|
if isinstance(response.json(), list):
|
|
return True, response.json()[0]
|
|
# This shouldn't happen, but is here just in case
|
|
else:
|
|
return True, response.json()
|
|
else:
|
|
return False, response.json().get("message", "")
|
|
|
|
|
|
def ceph_pool_list(config, limit):
|
|
"""
|
|
Get list information about Ceph pools (limited by {limit})
|
|
|
|
API endpoint: GET /api/v1/storage/ceph/pool
|
|
API arguments: limit={limit}
|
|
API schema: [{json_data_object},{json_data_object},etc.]
|
|
"""
|
|
params = dict()
|
|
if limit:
|
|
params["limit"] = limit
|
|
|
|
response = call_api(config, "get", "/storage/ceph/pool", params=params)
|
|
|
|
if response.status_code == 200:
|
|
return True, response.json()
|
|
else:
|
|
return False, response.json().get("message", "")
|
|
|
|
|
|
def ceph_pool_add(config, pool, pgs, replcfg, tier):
|
|
"""
|
|
Add new Ceph pool
|
|
|
|
API endpoint: POST /api/v1/storage/ceph/pool
|
|
API arguments: pool={pool}, pgs={pgs}, replcfg={replcfg}, tier={tier}
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {"pool": pool, "pgs": pgs, "replcfg": replcfg, "tier": tier}
|
|
response = call_api(config, "post", "/storage/ceph/pool", params=params)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_pool_remove(config, pool):
|
|
"""
|
|
Remove Ceph pool
|
|
|
|
API endpoint: DELETE /api/v1/storage/ceph/pool/{pool}
|
|
API arguments:
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {"yes-i-really-mean-it": "yes"}
|
|
response = call_api(
|
|
config, "delete", "/storage/ceph/pool/{pool}".format(pool=pool), params=params
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_pool_set_pgs(config, pool, pgs):
|
|
"""
|
|
Set the PGs of a Ceph pool
|
|
|
|
API endpoint: PUT /api/v1/storage/ceph/pool/{pool}
|
|
API arguments: {"pgs": "{pgs}"}
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {"pgs": pgs}
|
|
response = call_api(
|
|
config, "put", "/storage/ceph/pool/{pool}".format(pool=pool), params=params
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def format_list_pool(pool_list):
|
|
# Handle empty list
|
|
if not pool_list:
|
|
pool_list = list()
|
|
|
|
pool_list_output = []
|
|
|
|
pool_name_length = 5
|
|
pool_id_length = 3
|
|
pool_tier_length = 5
|
|
pool_pgs_length = 4
|
|
pool_used_length = 5
|
|
pool_usedpct_length = 6
|
|
pool_free_length = 5
|
|
pool_num_objects_length = 6
|
|
pool_num_clones_length = 7
|
|
pool_num_copies_length = 7
|
|
pool_num_degraded_length = 9
|
|
pool_read_ops_length = 4
|
|
pool_read_data_length = 5
|
|
pool_write_ops_length = 4
|
|
pool_write_data_length = 5
|
|
|
|
for pool_information in pool_list:
|
|
# Deal with the size to human readable
|
|
for datatype in ["free_bytes", "used_bytes", "write_bytes", "read_bytes"]:
|
|
databytes = pool_information["stats"][datatype]
|
|
databytes_formatted = format_bytes_tohuman(int(databytes))
|
|
pool_information["stats"][datatype] = databytes_formatted
|
|
for datatype in ["write_ops", "read_ops"]:
|
|
dataops = pool_information["stats"][datatype]
|
|
dataops_formatted = format_ops_tohuman(int(dataops))
|
|
pool_information["stats"][datatype] = dataops_formatted
|
|
for datatype in ["used_percent"]:
|
|
datapct = pool_information["stats"][datatype]
|
|
datapct_formatted = format_pct_tohuman(float(datapct))
|
|
pool_information["stats"][datatype] = datapct_formatted
|
|
|
|
# Set the Pool name length
|
|
_pool_name_length = len(pool_information["name"]) + 1
|
|
if _pool_name_length > pool_name_length:
|
|
pool_name_length = _pool_name_length
|
|
|
|
# Set the id and length
|
|
_pool_id_length = len(str(pool_information["stats"]["id"])) + 1
|
|
if _pool_id_length > pool_id_length:
|
|
pool_id_length = _pool_id_length
|
|
|
|
# Set the tier and length
|
|
_pool_tier_length = len(str(pool_information["tier"])) + 1
|
|
if _pool_tier_length > pool_tier_length:
|
|
pool_tier_length = _pool_tier_length
|
|
|
|
# Set the pgs and length
|
|
_pool_pgs_length = len(str(pool_information["pgs"])) + 1
|
|
if _pool_pgs_length > pool_pgs_length:
|
|
pool_pgs_length = _pool_pgs_length
|
|
|
|
# Set the used and length
|
|
_pool_used_length = len(str(pool_information["stats"]["used_bytes"])) + 1
|
|
if _pool_used_length > pool_used_length:
|
|
pool_used_length = _pool_used_length
|
|
|
|
# Set the usedpct and length
|
|
_pool_usedpct_length = len(str(pool_information["stats"]["used_percent"])) + 1
|
|
if _pool_usedpct_length > pool_usedpct_length:
|
|
pool_usedpct_length = _pool_usedpct_length
|
|
|
|
# Set the free and length
|
|
_pool_free_length = len(str(pool_information["stats"]["free_bytes"])) + 1
|
|
if _pool_free_length > pool_free_length:
|
|
pool_free_length = _pool_free_length
|
|
|
|
# Set the num_objects and length
|
|
_pool_num_objects_length = (
|
|
len(str(pool_information["stats"]["num_objects"])) + 1
|
|
)
|
|
if _pool_num_objects_length > pool_num_objects_length:
|
|
pool_num_objects_length = _pool_num_objects_length
|
|
|
|
# Set the num_clones and length
|
|
_pool_num_clones_length = (
|
|
len(str(pool_information["stats"]["num_object_clones"])) + 1
|
|
)
|
|
if _pool_num_clones_length > pool_num_clones_length:
|
|
pool_num_clones_length = _pool_num_clones_length
|
|
|
|
# Set the num_copies and length
|
|
_pool_num_copies_length = (
|
|
len(str(pool_information["stats"]["num_object_copies"])) + 1
|
|
)
|
|
if _pool_num_copies_length > pool_num_copies_length:
|
|
pool_num_copies_length = _pool_num_copies_length
|
|
|
|
# Set the num_degraded and length
|
|
_pool_num_degraded_length = (
|
|
len(str(pool_information["stats"]["num_objects_degraded"])) + 1
|
|
)
|
|
if _pool_num_degraded_length > pool_num_degraded_length:
|
|
pool_num_degraded_length = _pool_num_degraded_length
|
|
|
|
# Set the read/write IOPS/data and length
|
|
_pool_write_ops_length = len(str(pool_information["stats"]["write_ops"])) + 1
|
|
if _pool_write_ops_length > pool_write_ops_length:
|
|
pool_write_ops_length = _pool_write_ops_length
|
|
|
|
_pool_write_data_length = len(pool_information["stats"]["write_bytes"]) + 1
|
|
if _pool_write_data_length > pool_write_data_length:
|
|
pool_write_data_length = _pool_write_data_length
|
|
|
|
_pool_read_ops_length = len(str(pool_information["stats"]["read_ops"])) + 1
|
|
if _pool_read_ops_length > pool_read_ops_length:
|
|
pool_read_ops_length = _pool_read_ops_length
|
|
|
|
_pool_read_data_length = len(pool_information["stats"]["read_bytes"]) + 1
|
|
if _pool_read_data_length > pool_read_data_length:
|
|
pool_read_data_length = _pool_read_data_length
|
|
|
|
# Format the output header
|
|
pool_list_output.append(
|
|
"{bold}{pool_header: <{pool_header_length}} {objects_header: <{objects_header_length}} {read_header: <{read_header_length}} {write_header: <{write_header_length}}{end_bold}".format(
|
|
bold=ansiprint.bold(),
|
|
end_bold=ansiprint.end(),
|
|
pool_header_length=pool_id_length
|
|
+ pool_name_length
|
|
+ pool_tier_length
|
|
+ pool_pgs_length
|
|
+ pool_used_length
|
|
+ pool_usedpct_length
|
|
+ pool_free_length
|
|
+ 6,
|
|
objects_header_length=pool_num_objects_length
|
|
+ pool_num_clones_length
|
|
+ pool_num_copies_length
|
|
+ pool_num_degraded_length
|
|
+ 3,
|
|
read_header_length=pool_read_ops_length + pool_read_data_length + 1,
|
|
write_header_length=pool_write_ops_length + pool_write_data_length + 1,
|
|
pool_header="Pools "
|
|
+ "".join(
|
|
[
|
|
"-"
|
|
for _ in range(
|
|
6,
|
|
pool_id_length
|
|
+ pool_name_length
|
|
+ pool_tier_length
|
|
+ pool_pgs_length
|
|
+ pool_used_length
|
|
+ pool_usedpct_length
|
|
+ pool_free_length
|
|
+ 5,
|
|
)
|
|
]
|
|
),
|
|
objects_header="Objects "
|
|
+ "".join(
|
|
[
|
|
"-"
|
|
for _ in range(
|
|
8,
|
|
pool_num_objects_length
|
|
+ pool_num_clones_length
|
|
+ pool_num_copies_length
|
|
+ pool_num_degraded_length
|
|
+ 2,
|
|
)
|
|
]
|
|
),
|
|
read_header="Read "
|
|
+ "".join(
|
|
["-" for _ in range(5, pool_read_ops_length + pool_read_data_length)]
|
|
),
|
|
write_header="Write "
|
|
+ "".join(
|
|
["-" for _ in range(6, pool_write_ops_length + pool_write_data_length)]
|
|
),
|
|
)
|
|
)
|
|
|
|
pool_list_output.append(
|
|
"{bold}\
|
|
{pool_id: <{pool_id_length}} \
|
|
{pool_name: <{pool_name_length}} \
|
|
{pool_tier: <{pool_tier_length}} \
|
|
{pool_pgs: <{pool_pgs_length}} \
|
|
{pool_used: <{pool_used_length}} \
|
|
{pool_usedpct: <{pool_usedpct_length}} \
|
|
{pool_free: <{pool_free_length}} \
|
|
{pool_objects: <{pool_objects_length}} \
|
|
{pool_clones: <{pool_clones_length}} \
|
|
{pool_copies: <{pool_copies_length}} \
|
|
{pool_degraded: <{pool_degraded_length}} \
|
|
{pool_read_ops: <{pool_read_ops_length}} \
|
|
{pool_read_data: <{pool_read_data_length}} \
|
|
{pool_write_ops: <{pool_write_ops_length}} \
|
|
{pool_write_data: <{pool_write_data_length}} \
|
|
{end_bold}".format(
|
|
bold=ansiprint.bold(),
|
|
end_bold=ansiprint.end(),
|
|
pool_id_length=pool_id_length,
|
|
pool_name_length=pool_name_length,
|
|
pool_tier_length=pool_tier_length,
|
|
pool_pgs_length=pool_pgs_length,
|
|
pool_used_length=pool_used_length,
|
|
pool_usedpct_length=pool_usedpct_length,
|
|
pool_free_length=pool_free_length,
|
|
pool_objects_length=pool_num_objects_length,
|
|
pool_clones_length=pool_num_clones_length,
|
|
pool_copies_length=pool_num_copies_length,
|
|
pool_degraded_length=pool_num_degraded_length,
|
|
pool_write_ops_length=pool_write_ops_length,
|
|
pool_write_data_length=pool_write_data_length,
|
|
pool_read_ops_length=pool_read_ops_length,
|
|
pool_read_data_length=pool_read_data_length,
|
|
pool_id="ID",
|
|
pool_name="Name",
|
|
pool_tier="Tier",
|
|
pool_pgs="PGs",
|
|
pool_used="Used",
|
|
pool_usedpct="Used%",
|
|
pool_free="Free",
|
|
pool_objects="Count",
|
|
pool_clones="Clones",
|
|
pool_copies="Copies",
|
|
pool_degraded="Degraded",
|
|
pool_write_ops="OPS",
|
|
pool_write_data="Data",
|
|
pool_read_ops="OPS",
|
|
pool_read_data="Data",
|
|
)
|
|
)
|
|
|
|
for pool_information in sorted(pool_list, key=lambda x: int(x["stats"]["id"])):
|
|
# Format the output header
|
|
pool_list_output.append(
|
|
"{bold}\
|
|
{pool_id: <{pool_id_length}} \
|
|
{pool_name: <{pool_name_length}} \
|
|
{pool_tier: <{pool_tier_length}} \
|
|
{pool_pgs: <{pool_pgs_length}} \
|
|
{pool_used: <{pool_used_length}} \
|
|
{pool_usedpct: <{pool_usedpct_length}} \
|
|
{pool_free: <{pool_free_length}} \
|
|
{pool_objects: <{pool_objects_length}} \
|
|
{pool_clones: <{pool_clones_length}} \
|
|
{pool_copies: <{pool_copies_length}} \
|
|
{pool_degraded: <{pool_degraded_length}} \
|
|
{pool_read_ops: <{pool_read_ops_length}} \
|
|
{pool_read_data: <{pool_read_data_length}} \
|
|
{pool_write_ops: <{pool_write_ops_length}} \
|
|
{pool_write_data: <{pool_write_data_length}} \
|
|
{end_bold}".format(
|
|
bold="",
|
|
end_bold="",
|
|
pool_id_length=pool_id_length,
|
|
pool_name_length=pool_name_length,
|
|
pool_tier_length=pool_tier_length,
|
|
pool_pgs_length=pool_pgs_length,
|
|
pool_used_length=pool_used_length,
|
|
pool_usedpct_length=pool_usedpct_length,
|
|
pool_free_length=pool_free_length,
|
|
pool_objects_length=pool_num_objects_length,
|
|
pool_clones_length=pool_num_clones_length,
|
|
pool_copies_length=pool_num_copies_length,
|
|
pool_degraded_length=pool_num_degraded_length,
|
|
pool_write_ops_length=pool_write_ops_length,
|
|
pool_write_data_length=pool_write_data_length,
|
|
pool_read_ops_length=pool_read_ops_length,
|
|
pool_read_data_length=pool_read_data_length,
|
|
pool_id=pool_information["stats"]["id"],
|
|
pool_name=pool_information["name"],
|
|
pool_tier=pool_information["tier"],
|
|
pool_pgs=pool_information["pgs"],
|
|
pool_used=pool_information["stats"]["used_bytes"],
|
|
pool_usedpct=pool_information["stats"]["used_percent"],
|
|
pool_free=pool_information["stats"]["free_bytes"],
|
|
pool_objects=pool_information["stats"]["num_objects"],
|
|
pool_clones=pool_information["stats"]["num_object_clones"],
|
|
pool_copies=pool_information["stats"]["num_object_copies"],
|
|
pool_degraded=pool_information["stats"]["num_objects_degraded"],
|
|
pool_write_ops=pool_information["stats"]["write_ops"],
|
|
pool_write_data=pool_information["stats"]["write_bytes"],
|
|
pool_read_ops=pool_information["stats"]["read_ops"],
|
|
pool_read_data=pool_information["stats"]["read_bytes"],
|
|
)
|
|
)
|
|
|
|
return "\n".join(pool_list_output)
|
|
|
|
|
|
#
|
|
# Volume functions
|
|
#
|
|
def ceph_volume_info(config, pool, volume):
|
|
"""
|
|
Get information about Ceph volume
|
|
|
|
API endpoint: GET /api/v1/storage/ceph/volume/{pool}/{volume}
|
|
API arguments:
|
|
API schema: {json_data_object}
|
|
"""
|
|
response = call_api(
|
|
config,
|
|
"get",
|
|
"/storage/ceph/volume/{pool}/{volume}".format(volume=volume, pool=pool),
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
if isinstance(response.json(), list) and len(response.json()) != 1:
|
|
# No exact match; return not found
|
|
return False, "Volume not found."
|
|
else:
|
|
# Return a single instance if the response is a list
|
|
if isinstance(response.json(), list):
|
|
return True, response.json()[0]
|
|
# This shouldn't happen, but is here just in case
|
|
else:
|
|
return True, response.json()
|
|
else:
|
|
return False, response.json().get("message", "")
|
|
|
|
|
|
def ceph_volume_list(config, limit, pool):
|
|
"""
|
|
Get list information about Ceph volumes (limited by {limit} and by {pool})
|
|
|
|
API endpoint: GET /api/v1/storage/ceph/volume
|
|
API arguments: limit={limit}, pool={pool}
|
|
API schema: [{json_data_object},{json_data_object},etc.]
|
|
"""
|
|
params = dict()
|
|
if limit:
|
|
params["limit"] = limit
|
|
if pool:
|
|
params["pool"] = pool
|
|
|
|
response = call_api(config, "get", "/storage/ceph/volume", params=params)
|
|
|
|
if response.status_code == 200:
|
|
return True, response.json()
|
|
else:
|
|
return False, response.json().get("message", "")
|
|
|
|
|
|
def ceph_volume_add(config, pool, volume, size):
|
|
"""
|
|
Add new Ceph volume
|
|
|
|
API endpoint: POST /api/v1/storage/ceph/volume
|
|
API arguments: volume={volume}, pool={pool}, size={size}
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {"volume": volume, "pool": pool, "size": size}
|
|
response = call_api(config, "post", "/storage/ceph/volume", params=params)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_volume_upload(config, pool, volume, image_format, image_file):
|
|
"""
|
|
Upload a disk image to a Ceph volume
|
|
|
|
API endpoint: POST /api/v1/storage/ceph/volume/{pool}/{volume}/upload
|
|
API arguments: image_format={image_format}
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
import click
|
|
|
|
bar = UploadProgressBar(
|
|
image_file, end_message="Parsing file on remote side...", end_nl=False
|
|
)
|
|
upload_data = MultipartEncoder(
|
|
fields={
|
|
"file": ("filename", open(image_file, "rb"), "application/octet-stream")
|
|
}
|
|
)
|
|
upload_monitor = MultipartEncoderMonitor(upload_data, bar.update)
|
|
|
|
headers = {"Content-Type": upload_monitor.content_type}
|
|
params = {"image_format": image_format}
|
|
|
|
response = call_api(
|
|
config,
|
|
"post",
|
|
"/storage/ceph/volume/{}/{}/upload".format(pool, volume),
|
|
headers=headers,
|
|
params=params,
|
|
data=upload_monitor,
|
|
)
|
|
|
|
click.echo("done.")
|
|
click.echo()
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_volume_remove(config, pool, volume):
|
|
"""
|
|
Remove Ceph volume
|
|
|
|
API endpoint: DELETE /api/v1/storage/ceph/volume/{pool}/{volume}
|
|
API arguments:
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
response = call_api(
|
|
config,
|
|
"delete",
|
|
"/storage/ceph/volume/{pool}/{volume}".format(volume=volume, pool=pool),
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_volume_modify(config, pool, volume, new_name=None, new_size=None):
|
|
"""
|
|
Modify Ceph volume
|
|
|
|
API endpoint: PUT /api/v1/storage/ceph/volume/{pool}/{volume}
|
|
API arguments:
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
|
|
params = dict()
|
|
if new_name:
|
|
params["new_name"] = new_name
|
|
if new_size:
|
|
params["new_size"] = new_size
|
|
|
|
response = call_api(
|
|
config,
|
|
"put",
|
|
"/storage/ceph/volume/{pool}/{volume}".format(volume=volume, pool=pool),
|
|
params=params,
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_volume_clone(config, pool, volume, new_volume):
|
|
"""
|
|
Clone Ceph volume
|
|
|
|
API endpoint: POST /api/v1/storage/ceph/volume/{pool}/{volume}
|
|
API arguments: new_volume={new_volume
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {"new_volume": new_volume}
|
|
response = call_api(
|
|
config,
|
|
"post",
|
|
"/storage/ceph/volume/{pool}/{volume}/clone".format(volume=volume, pool=pool),
|
|
params=params,
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def format_list_volume(volume_list):
|
|
# Handle empty list
|
|
if not volume_list:
|
|
volume_list = list()
|
|
|
|
volume_list_output = []
|
|
|
|
volume_name_length = 5
|
|
volume_pool_length = 5
|
|
volume_size_length = 5
|
|
volume_objects_length = 8
|
|
volume_order_length = 6
|
|
volume_format_length = 7
|
|
volume_features_length = 10
|
|
|
|
for volume_information in volume_list:
|
|
# Set the Volume name length
|
|
_volume_name_length = len(volume_information["name"]) + 1
|
|
if _volume_name_length > volume_name_length:
|
|
volume_name_length = _volume_name_length
|
|
|
|
# Set the Volume pool length
|
|
_volume_pool_length = len(volume_information["pool"]) + 1
|
|
if _volume_pool_length > volume_pool_length:
|
|
volume_pool_length = _volume_pool_length
|
|
|
|
# Set the size and length
|
|
_volume_size_length = len(str(volume_information["stats"]["size"])) + 1
|
|
if _volume_size_length > volume_size_length:
|
|
volume_size_length = _volume_size_length
|
|
|
|
# Set the num_objects and length
|
|
_volume_objects_length = len(str(volume_information["stats"]["objects"])) + 1
|
|
if _volume_objects_length > volume_objects_length:
|
|
volume_objects_length = _volume_objects_length
|
|
|
|
# Set the order and length
|
|
_volume_order_length = len(str(volume_information["stats"]["order"])) + 1
|
|
if _volume_order_length > volume_order_length:
|
|
volume_order_length = _volume_order_length
|
|
|
|
# Set the format and length
|
|
_volume_format_length = len(str(volume_information["stats"]["format"])) + 1
|
|
if _volume_format_length > volume_format_length:
|
|
volume_format_length = _volume_format_length
|
|
|
|
# Set the features and length
|
|
_volume_features_length = (
|
|
len(str(",".join(volume_information["stats"]["features"]))) + 1
|
|
)
|
|
if _volume_features_length > volume_features_length:
|
|
volume_features_length = _volume_features_length
|
|
|
|
# Format the output header
|
|
volume_list_output.append(
|
|
"{bold}{volume_header: <{volume_header_length}} {details_header: <{details_header_length}}{end_bold}".format(
|
|
bold=ansiprint.bold(),
|
|
end_bold=ansiprint.end(),
|
|
volume_header_length=volume_name_length + volume_pool_length + 1,
|
|
details_header_length=volume_size_length
|
|
+ volume_objects_length
|
|
+ volume_order_length
|
|
+ volume_format_length
|
|
+ volume_features_length
|
|
+ 4,
|
|
volume_header="Volumes "
|
|
+ "".join(["-" for _ in range(8, volume_name_length + volume_pool_length)]),
|
|
details_header="Details "
|
|
+ "".join(
|
|
[
|
|
"-"
|
|
for _ in range(
|
|
8,
|
|
volume_size_length
|
|
+ volume_objects_length
|
|
+ volume_order_length
|
|
+ volume_format_length
|
|
+ volume_features_length
|
|
+ 3,
|
|
)
|
|
]
|
|
),
|
|
)
|
|
)
|
|
|
|
volume_list_output.append(
|
|
"{bold}\
|
|
{volume_name: <{volume_name_length}} \
|
|
{volume_pool: <{volume_pool_length}} \
|
|
{volume_size: <{volume_size_length}} \
|
|
{volume_objects: <{volume_objects_length}} \
|
|
{volume_order: <{volume_order_length}} \
|
|
{volume_format: <{volume_format_length}} \
|
|
{volume_features: <{volume_features_length}} \
|
|
{end_bold}".format(
|
|
bold=ansiprint.bold(),
|
|
end_bold=ansiprint.end(),
|
|
volume_name_length=volume_name_length,
|
|
volume_pool_length=volume_pool_length,
|
|
volume_size_length=volume_size_length,
|
|
volume_objects_length=volume_objects_length,
|
|
volume_order_length=volume_order_length,
|
|
volume_format_length=volume_format_length,
|
|
volume_features_length=volume_features_length,
|
|
volume_name="Name",
|
|
volume_pool="Pool",
|
|
volume_size="Size",
|
|
volume_objects="Objects",
|
|
volume_order="Order",
|
|
volume_format="Format",
|
|
volume_features="Features",
|
|
)
|
|
)
|
|
|
|
for volume_information in sorted(volume_list, key=lambda v: v["pool"] + v["name"]):
|
|
volume_list_output.append(
|
|
"{bold}\
|
|
{volume_name: <{volume_name_length}} \
|
|
{volume_pool: <{volume_pool_length}} \
|
|
{volume_size: <{volume_size_length}} \
|
|
{volume_objects: <{volume_objects_length}} \
|
|
{volume_order: <{volume_order_length}} \
|
|
{volume_format: <{volume_format_length}} \
|
|
{volume_features: <{volume_features_length}} \
|
|
{end_bold}".format(
|
|
bold="",
|
|
end_bold="",
|
|
volume_name_length=volume_name_length,
|
|
volume_pool_length=volume_pool_length,
|
|
volume_size_length=volume_size_length,
|
|
volume_objects_length=volume_objects_length,
|
|
volume_order_length=volume_order_length,
|
|
volume_format_length=volume_format_length,
|
|
volume_features_length=volume_features_length,
|
|
volume_name=volume_information["name"],
|
|
volume_pool=volume_information["pool"],
|
|
volume_size=volume_information["stats"]["size"],
|
|
volume_objects=volume_information["stats"]["objects"],
|
|
volume_order=volume_information["stats"]["order"],
|
|
volume_format=volume_information["stats"]["format"],
|
|
volume_features=",".join(volume_information["stats"]["features"]),
|
|
)
|
|
)
|
|
|
|
return "\n".join(volume_list_output)
|
|
|
|
|
|
#
|
|
# Snapshot functions
|
|
#
|
|
def ceph_snapshot_info(config, pool, volume, snapshot):
|
|
"""
|
|
Get information about Ceph snapshot
|
|
|
|
API endpoint: GET /api/v1/storage/ceph/snapshot/{pool}/{volume}/{snapshot}
|
|
API arguments:
|
|
API schema: {json_data_object}
|
|
"""
|
|
response = call_api(
|
|
config,
|
|
"get",
|
|
"/storage/ceph/snapshot/{pool}/{volume}/{snapshot}".format(
|
|
snapshot=snapshot, volume=volume, pool=pool
|
|
),
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
if isinstance(response.json(), list) and len(response.json()) != 1:
|
|
# No exact match; return not found
|
|
return False, "Snapshot not found."
|
|
else:
|
|
# Return a single instance if the response is a list
|
|
if isinstance(response.json(), list):
|
|
return True, response.json()[0]
|
|
# This shouldn't happen, but is here just in case
|
|
else:
|
|
return True, response.json()
|
|
else:
|
|
return False, response.json().get("message", "")
|
|
|
|
|
|
def ceph_snapshot_list(config, limit, volume, pool):
|
|
"""
|
|
Get list information about Ceph snapshots (limited by {limit}, by {pool}, or by {volume})
|
|
|
|
API endpoint: GET /api/v1/storage/ceph/snapshot
|
|
API arguments: limit={limit}, volume={volume}, pool={pool}
|
|
API schema: [{json_data_object},{json_data_object},etc.]
|
|
"""
|
|
params = dict()
|
|
if limit:
|
|
params["limit"] = limit
|
|
if volume:
|
|
params["volume"] = volume
|
|
if pool:
|
|
params["pool"] = pool
|
|
|
|
response = call_api(config, "get", "/storage/ceph/snapshot", params=params)
|
|
|
|
if response.status_code == 200:
|
|
return True, response.json()
|
|
else:
|
|
return False, response.json().get("message", "")
|
|
|
|
|
|
def ceph_snapshot_add(config, pool, volume, snapshot):
|
|
"""
|
|
Add new Ceph snapshot
|
|
|
|
API endpoint: POST /api/v1/storage/ceph/snapshot
|
|
API arguments: snapshot={snapshot}, volume={volume}, pool={pool}
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
params = {"snapshot": snapshot, "volume": volume, "pool": pool}
|
|
response = call_api(config, "post", "/storage/ceph/snapshot", params=params)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_snapshot_remove(config, pool, volume, snapshot):
|
|
"""
|
|
Remove Ceph snapshot
|
|
|
|
API endpoint: DELETE /api/v1/storage/ceph/snapshot/{pool}/{volume}/{snapshot}
|
|
API arguments:
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
response = call_api(
|
|
config,
|
|
"delete",
|
|
"/storage/ceph/snapshot/{pool}/{volume}/{snapshot}".format(
|
|
snapshot=snapshot, volume=volume, pool=pool
|
|
),
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def ceph_snapshot_modify(config, pool, volume, snapshot, new_name=None):
|
|
"""
|
|
Modify Ceph snapshot
|
|
|
|
API endpoint: PUT /api/v1/storage/ceph/snapshot/{pool}/{volume}/{snapshot}
|
|
API arguments:
|
|
API schema: {"message":"{data}"}
|
|
"""
|
|
|
|
params = dict()
|
|
if new_name:
|
|
params["new_name"] = new_name
|
|
|
|
response = call_api(
|
|
config,
|
|
"put",
|
|
"/storage/ceph/snapshot/{pool}/{volume}/{snapshot}".format(
|
|
snapshot=snapshot, volume=volume, pool=pool
|
|
),
|
|
params=params,
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
retstatus = True
|
|
else:
|
|
retstatus = False
|
|
|
|
return retstatus, response.json().get("message", "")
|
|
|
|
|
|
def format_list_snapshot(snapshot_list):
|
|
# Handle empty list
|
|
if not snapshot_list:
|
|
snapshot_list = list()
|
|
|
|
snapshot_list_output = []
|
|
|
|
snapshot_name_length = 5
|
|
snapshot_volume_length = 7
|
|
snapshot_pool_length = 5
|
|
|
|
for snapshot_information in snapshot_list:
|
|
snapshot_name = snapshot_information["snapshot"]
|
|
snapshot_volume = snapshot_information["volume"]
|
|
snapshot_pool = snapshot_information["pool"]
|
|
|
|
# Set the Snapshot name length
|
|
_snapshot_name_length = len(snapshot_name) + 1
|
|
if _snapshot_name_length > snapshot_name_length:
|
|
snapshot_name_length = _snapshot_name_length
|
|
|
|
# Set the Snapshot volume length
|
|
_snapshot_volume_length = len(snapshot_volume) + 1
|
|
if _snapshot_volume_length > snapshot_volume_length:
|
|
snapshot_volume_length = _snapshot_volume_length
|
|
|
|
# Set the Snapshot pool length
|
|
_snapshot_pool_length = len(snapshot_pool) + 1
|
|
if _snapshot_pool_length > snapshot_pool_length:
|
|
snapshot_pool_length = _snapshot_pool_length
|
|
|
|
# Format the output header
|
|
snapshot_list_output.append(
|
|
"{bold}{snapshot_header: <{snapshot_header_length}}{end_bold}".format(
|
|
bold=ansiprint.bold(),
|
|
end_bold=ansiprint.end(),
|
|
snapshot_header_length=snapshot_name_length
|
|
+ snapshot_volume_length
|
|
+ snapshot_pool_length
|
|
+ 2,
|
|
snapshot_header="Snapshots "
|
|
+ "".join(
|
|
[
|
|
"-"
|
|
for _ in range(
|
|
10,
|
|
snapshot_name_length
|
|
+ snapshot_volume_length
|
|
+ snapshot_pool_length
|
|
+ 1,
|
|
)
|
|
]
|
|
),
|
|
)
|
|
)
|
|
|
|
snapshot_list_output.append(
|
|
"{bold}\
|
|
{snapshot_name: <{snapshot_name_length}} \
|
|
{snapshot_volume: <{snapshot_volume_length}} \
|
|
{snapshot_pool: <{snapshot_pool_length}} \
|
|
{end_bold}".format(
|
|
bold=ansiprint.bold(),
|
|
end_bold=ansiprint.end(),
|
|
snapshot_name_length=snapshot_name_length,
|
|
snapshot_volume_length=snapshot_volume_length,
|
|
snapshot_pool_length=snapshot_pool_length,
|
|
snapshot_name="Name",
|
|
snapshot_volume="Volume",
|
|
snapshot_pool="Pool",
|
|
)
|
|
)
|
|
|
|
for snapshot_information in sorted(
|
|
snapshot_list, key=lambda s: s["pool"] + s["volume"] + s["snapshot"]
|
|
):
|
|
snapshot_name = snapshot_information["snapshot"]
|
|
snapshot_volume = snapshot_information["volume"]
|
|
snapshot_pool = snapshot_information["pool"]
|
|
snapshot_list_output.append(
|
|
"{bold}\
|
|
{snapshot_name: <{snapshot_name_length}} \
|
|
{snapshot_volume: <{snapshot_volume_length}} \
|
|
{snapshot_pool: <{snapshot_pool_length}} \
|
|
{end_bold}".format(
|
|
bold="",
|
|
end_bold="",
|
|
snapshot_name_length=snapshot_name_length,
|
|
snapshot_volume_length=snapshot_volume_length,
|
|
snapshot_pool_length=snapshot_pool_length,
|
|
snapshot_name=snapshot_name,
|
|
snapshot_volume=snapshot_volume,
|
|
snapshot_pool=snapshot_pool,
|
|
)
|
|
)
|
|
|
|
return "\n".join(snapshot_list_output)
|
|
|
|
|
|
#
|
|
# Benchmark functions
|
|
#
|
|
def ceph_benchmark_run(config, pool):
|
|
"""
|
|
Run a storage benchmark against {pool}
|
|
|
|
API endpoint: POST /api/v1/storage/ceph/benchmark
|
|
API arguments: pool={pool}
|
|
API schema: {message}
|
|
"""
|
|
params = {"pool": pool}
|
|
response = call_api(config, "post", "/storage/ceph/benchmark", params=params)
|
|
|
|
if response.status_code == 202:
|
|
retvalue = True
|
|
retdata = "Task ID: {}".format(response.json()["task_id"])
|
|
else:
|
|
retvalue = False
|
|
retdata = response.json().get("message", "")
|
|
|
|
return retvalue, retdata
|
|
|
|
|
|
def ceph_benchmark_list(config, job):
|
|
"""
|
|
View results of one or more previous benchmark runs
|
|
|
|
API endpoint: GET /api/v1/storage/ceph/benchmark
|
|
API arguments: job={job}
|
|
API schema: {results}
|
|
"""
|
|
if job is not None:
|
|
params = {"job": job}
|
|
else:
|
|
params = {}
|
|
|
|
response = call_api(config, "get", "/storage/ceph/benchmark", params=params)
|
|
|
|
if response.status_code == 200:
|
|
retvalue = True
|
|
retdata = response.json()
|
|
else:
|
|
retvalue = False
|
|
retdata = response.json().get("message", "")
|
|
|
|
return retvalue, retdata
|
|
|
|
|
|
def get_benchmark_list_results_legacy(benchmark_data):
|
|
if isinstance(benchmark_data, str):
|
|
benchmark_data = loads(benchmark_data)
|
|
benchmark_bandwidth = dict()
|
|
benchmark_iops = dict()
|
|
for test in ["seq_read", "seq_write", "rand_read_4K", "rand_write_4K"]:
|
|
benchmark_bandwidth[test] = format_bytes_tohuman(
|
|
int(benchmark_data[test]["overall"]["bandwidth"]) * 1024
|
|
)
|
|
benchmark_iops[test] = format_ops_tohuman(
|
|
int(benchmark_data[test]["overall"]["iops"])
|
|
)
|
|
|
|
return benchmark_bandwidth, benchmark_iops
|
|
|
|
|
|
def get_benchmark_list_results_json(benchmark_data):
|
|
benchmark_bandwidth = dict()
|
|
benchmark_iops = dict()
|
|
for test in ["seq_read", "seq_write", "rand_read_4K", "rand_write_4K"]:
|
|
benchmark_test_data = benchmark_data[test]
|
|
active_class = None
|
|
for io_class in ["read", "write"]:
|
|
if benchmark_test_data["jobs"][0][io_class]["io_bytes"] > 0:
|
|
active_class = io_class
|
|
if active_class is not None:
|
|
benchmark_bandwidth[test] = format_bytes_tohuman(
|
|
int(benchmark_test_data["jobs"][0][active_class]["bw_bytes"])
|
|
)
|
|
benchmark_iops[test] = format_ops_tohuman(
|
|
int(benchmark_test_data["jobs"][0][active_class]["iops"])
|
|
)
|
|
|
|
return benchmark_bandwidth, benchmark_iops
|
|
|
|
|
|
def get_benchmark_list_results(benchmark_format, benchmark_data):
|
|
if benchmark_format == 0:
|
|
benchmark_bandwidth, benchmark_iops = get_benchmark_list_results_legacy(
|
|
benchmark_data
|
|
)
|
|
elif benchmark_format == 1:
|
|
benchmark_bandwidth, benchmark_iops = get_benchmark_list_results_json(
|
|
benchmark_data
|
|
)
|
|
|
|
seq_benchmark_bandwidth = "{} / {}".format(
|
|
benchmark_bandwidth["seq_read"], benchmark_bandwidth["seq_write"]
|
|
)
|
|
seq_benchmark_iops = "{} / {}".format(
|
|
benchmark_iops["seq_read"], benchmark_iops["seq_write"]
|
|
)
|
|
rand_benchmark_bandwidth = "{} / {}".format(
|
|
benchmark_bandwidth["rand_read_4K"], benchmark_bandwidth["rand_write_4K"]
|
|
)
|
|
rand_benchmark_iops = "{} / {}".format(
|
|
benchmark_iops["rand_read_4K"], benchmark_iops["rand_write_4K"]
|
|
)
|
|
|
|
return (
|
|
seq_benchmark_bandwidth,
|
|
seq_benchmark_iops,
|
|
rand_benchmark_bandwidth,
|
|
rand_benchmark_iops,
|
|
)
|
|
|
|
|
|
def format_list_benchmark(config, benchmark_information):
|
|
benchmark_list_output = []
|
|
|
|
benchmark_job_length = 20
|
|
benchmark_format_length = 6
|
|
benchmark_bandwidth_length = dict()
|
|
benchmark_iops_length = dict()
|
|
|
|
# For this output, we're only showing the Sequential (seq_read and seq_write) and 4k Random (rand_read_4K and rand_write_4K) results since we're showing them for each test result.
|
|
for test in ["seq_read", "seq_write", "rand_read_4K", "rand_write_4K"]:
|
|
benchmark_bandwidth_length[test] = 7
|
|
benchmark_iops_length[test] = 6
|
|
|
|
benchmark_seq_bw_length = 15
|
|
benchmark_seq_iops_length = 10
|
|
benchmark_rand_bw_length = 15
|
|
benchmark_rand_iops_length = 10
|
|
|
|
for benchmark in benchmark_information:
|
|
benchmark_job = benchmark["job"]
|
|
benchmark_format = benchmark.get("test_format", 0) # noqa: F841
|
|
|
|
_benchmark_job_length = len(benchmark_job)
|
|
if _benchmark_job_length > benchmark_job_length:
|
|
benchmark_job_length = _benchmark_job_length
|
|
|
|
if benchmark["benchmark_result"] == "Running":
|
|
continue
|
|
|
|
benchmark_data = benchmark["benchmark_result"]
|
|
(
|
|
seq_benchmark_bandwidth,
|
|
seq_benchmark_iops,
|
|
rand_benchmark_bandwidth,
|
|
rand_benchmark_iops,
|
|
) = get_benchmark_list_results(benchmark_format, benchmark_data)
|
|
|
|
_benchmark_seq_bw_length = len(seq_benchmark_bandwidth) + 1
|
|
if _benchmark_seq_bw_length > benchmark_seq_bw_length:
|
|
benchmark_seq_bw_length = _benchmark_seq_bw_length
|
|
|
|
_benchmark_seq_iops_length = len(seq_benchmark_iops) + 1
|
|
if _benchmark_seq_iops_length > benchmark_seq_iops_length:
|
|
benchmark_seq_iops_length = _benchmark_seq_iops_length
|
|
|
|
_benchmark_rand_bw_length = len(rand_benchmark_bandwidth) + 1
|
|
if _benchmark_rand_bw_length > benchmark_rand_bw_length:
|
|
benchmark_rand_bw_length = _benchmark_rand_bw_length
|
|
|
|
_benchmark_rand_iops_length = len(rand_benchmark_iops) + 1
|
|
if _benchmark_rand_iops_length > benchmark_rand_iops_length:
|
|
benchmark_rand_iops_length = _benchmark_rand_iops_length
|
|
|
|
# Format the output header line 1
|
|
benchmark_list_output.append(
|
|
"{bold}\
|
|
{benchmark_job: <{benchmark_job_length}} \
|
|
{seq_header: <{seq_header_length}} \
|
|
{rand_header: <{rand_header_length}}\
|
|
{end_bold}".format(
|
|
bold=ansiprint.bold(),
|
|
end_bold=ansiprint.end(),
|
|
benchmark_job_length=benchmark_job_length + benchmark_format_length + 1,
|
|
seq_header_length=benchmark_seq_bw_length + benchmark_seq_iops_length + 1,
|
|
rand_header_length=benchmark_rand_bw_length
|
|
+ benchmark_rand_iops_length
|
|
+ 1,
|
|
benchmark_job="Benchmarks "
|
|
+ "".join(
|
|
[
|
|
"-"
|
|
for _ in range(
|
|
11, benchmark_job_length + benchmark_format_length + 2
|
|
)
|
|
]
|
|
),
|
|
seq_header="Sequential (4M blocks) "
|
|
+ "".join(
|
|
[
|
|
"-"
|
|
for _ in range(
|
|
23, benchmark_seq_bw_length + benchmark_seq_iops_length
|
|
)
|
|
]
|
|
),
|
|
rand_header="Random (4K blocks) "
|
|
+ "".join(
|
|
[
|
|
"-"
|
|
for _ in range(
|
|
19, benchmark_rand_bw_length + benchmark_rand_iops_length
|
|
)
|
|
]
|
|
),
|
|
)
|
|
)
|
|
|
|
benchmark_list_output.append(
|
|
"{bold}\
|
|
{benchmark_job: <{benchmark_job_length}} \
|
|
{benchmark_format: <{benchmark_format_length}} \
|
|
{seq_benchmark_bandwidth: <{seq_benchmark_bandwidth_length}} \
|
|
{seq_benchmark_iops: <{seq_benchmark_iops_length}} \
|
|
{rand_benchmark_bandwidth: <{rand_benchmark_bandwidth_length}} \
|
|
{rand_benchmark_iops: <{rand_benchmark_iops_length}}\
|
|
{end_bold}".format(
|
|
bold=ansiprint.bold(),
|
|
end_bold=ansiprint.end(),
|
|
benchmark_job_length=benchmark_job_length,
|
|
benchmark_format_length=benchmark_format_length,
|
|
seq_benchmark_bandwidth_length=benchmark_seq_bw_length,
|
|
seq_benchmark_iops_length=benchmark_seq_iops_length,
|
|
rand_benchmark_bandwidth_length=benchmark_rand_bw_length,
|
|
rand_benchmark_iops_length=benchmark_rand_iops_length,
|
|
benchmark_job="Job",
|
|
benchmark_format="Format",
|
|
seq_benchmark_bandwidth="R/W Bandwith/s",
|
|
seq_benchmark_iops="R/W IOPS",
|
|
rand_benchmark_bandwidth="R/W Bandwith/s",
|
|
rand_benchmark_iops="R/W IOPS",
|
|
)
|
|
)
|
|
|
|
for benchmark in benchmark_information:
|
|
benchmark_job = benchmark["job"]
|
|
benchmark_format = benchmark.get("test_format", 0) # noqa: F841
|
|
|
|
if benchmark["benchmark_result"] == "Running":
|
|
seq_benchmark_bandwidth = "Running"
|
|
seq_benchmark_iops = "Running"
|
|
rand_benchmark_bandwidth = "Running"
|
|
rand_benchmark_iops = "Running"
|
|
else:
|
|
benchmark_data = benchmark["benchmark_result"]
|
|
(
|
|
seq_benchmark_bandwidth,
|
|
seq_benchmark_iops,
|
|
rand_benchmark_bandwidth,
|
|
rand_benchmark_iops,
|
|
) = get_benchmark_list_results(benchmark_format, benchmark_data)
|
|
|
|
benchmark_list_output.append(
|
|
"{bold}\
|
|
{benchmark_job: <{benchmark_job_length}} \
|
|
{benchmark_format: <{benchmark_format_length}} \
|
|
{seq_benchmark_bandwidth: <{seq_benchmark_bandwidth_length}} \
|
|
{seq_benchmark_iops: <{seq_benchmark_iops_length}} \
|
|
{rand_benchmark_bandwidth: <{rand_benchmark_bandwidth_length}} \
|
|
{rand_benchmark_iops: <{rand_benchmark_iops_length}}\
|
|
{end_bold}".format(
|
|
bold="",
|
|
end_bold="",
|
|
benchmark_job_length=benchmark_job_length,
|
|
benchmark_format_length=benchmark_format_length,
|
|
seq_benchmark_bandwidth_length=benchmark_seq_bw_length,
|
|
seq_benchmark_iops_length=benchmark_seq_iops_length,
|
|
rand_benchmark_bandwidth_length=benchmark_rand_bw_length,
|
|
rand_benchmark_iops_length=benchmark_rand_iops_length,
|
|
benchmark_job=benchmark_job,
|
|
benchmark_format=benchmark_format,
|
|
seq_benchmark_bandwidth=seq_benchmark_bandwidth,
|
|
seq_benchmark_iops=seq_benchmark_iops,
|
|
rand_benchmark_bandwidth=rand_benchmark_bandwidth,
|
|
rand_benchmark_iops=rand_benchmark_iops,
|
|
)
|
|
)
|
|
|
|
return "\n".join(benchmark_list_output)
|
|
|
|
|
|
def format_info_benchmark(config, oformat, benchmark_information):
|
|
# This matrix is a list of the possible format functions for a benchmark result
|
|
# It is extensable in the future should newer formats be required.
|
|
benchmark_matrix = {
|
|
0: format_info_benchmark_legacy,
|
|
1: format_info_benchmark_json,
|
|
}
|
|
|
|
benchmark_version = benchmark_information[0]["test_format"]
|
|
|
|
if oformat == "json-pretty":
|
|
return dumps(benchmark_information, indent=4)
|
|
elif oformat == "json":
|
|
return dumps(benchmark_information)
|
|
else:
|
|
return benchmark_matrix[benchmark_version](config, benchmark_information[0])
|
|
|
|
|
|
def format_info_benchmark_legacy(config, benchmark_information):
|
|
if benchmark_information["benchmark_result"] == "Running":
|
|
return "Benchmark test is still running."
|
|
|
|
benchmark_details = benchmark_information["benchmark_result"]
|
|
|
|
# Format a nice output; do this line-by-line then concat the elements at the end
|
|
ainformation = []
|
|
ainformation.append(
|
|
"{}Storage Benchmark details:{}".format(ansiprint.bold(), ansiprint.end())
|
|
)
|
|
|
|
nice_test_name_map = {
|
|
"seq_read": "Sequential Read (4M blocks)",
|
|
"seq_write": "Sequential Write (4M blocks)",
|
|
"rand_read_4M": "Random Read (4M blocks)",
|
|
"rand_write_4M": "Random Write (4M blocks)",
|
|
"rand_read_4K": "Random Read (4K blocks)",
|
|
"rand_write_4K": "Random Write (4K blocks)",
|
|
"rand_read_4K_lowdepth": "Random Read (4K blocks, single-queue)",
|
|
"rand_write_4K_lowdepth": "Random Write (4K blocks, single-queue)",
|
|
}
|
|
|
|
test_name_length = 30
|
|
overall_label_length = 12
|
|
overall_column_length = 8
|
|
bandwidth_label_length = 9
|
|
bandwidth_column_length = 10
|
|
iops_column_length = 6
|
|
latency_column_length = 8
|
|
cpuutil_label_length = 11
|
|
cpuutil_column_length = 9
|
|
|
|
# Work around old results that did not have these tests
|
|
if "rand_read_4K_lowdepth" not in benchmark_details:
|
|
del nice_test_name_map["rand_read_4K_lowdepth"]
|
|
del nice_test_name_map["rand_write_4K_lowdepth"]
|
|
|
|
for test in benchmark_details:
|
|
# Work around old results that had these obsolete tests
|
|
if test == "rand_read_256K" or test == "rand_write_256K":
|
|
continue
|
|
|
|
_test_name_length = len(nice_test_name_map[test])
|
|
if _test_name_length > test_name_length:
|
|
test_name_length = _test_name_length
|
|
|
|
for element in benchmark_details[test]["overall"]:
|
|
_element_length = len(benchmark_details[test]["overall"][element])
|
|
if _element_length > overall_column_length:
|
|
overall_column_length = _element_length
|
|
|
|
for element in benchmark_details[test]["bandwidth"]:
|
|
try:
|
|
_element_length = len(
|
|
format_bytes_tohuman(
|
|
int(float(benchmark_details[test]["bandwidth"][element]))
|
|
)
|
|
)
|
|
except Exception:
|
|
_element_length = len(benchmark_details[test]["bandwidth"][element])
|
|
if _element_length > bandwidth_column_length:
|
|
bandwidth_column_length = _element_length
|
|
|
|
for element in benchmark_details[test]["iops"]:
|
|
try:
|
|
_element_length = len(
|
|
format_ops_tohuman(
|
|
int(float(benchmark_details[test]["iops"][element]))
|
|
)
|
|
)
|
|
except Exception:
|
|
_element_length = len(benchmark_details[test]["iops"][element])
|
|
if _element_length > iops_column_length:
|
|
iops_column_length = _element_length
|
|
|
|
for element in benchmark_details[test]["latency"]:
|
|
_element_length = len(benchmark_details[test]["latency"][element])
|
|
if _element_length > latency_column_length:
|
|
latency_column_length = _element_length
|
|
|
|
for element in benchmark_details[test]["cpu"]:
|
|
_element_length = len(benchmark_details[test]["cpu"][element])
|
|
if _element_length > cpuutil_column_length:
|
|
cpuutil_column_length = _element_length
|
|
|
|
for test in benchmark_details:
|
|
# Work around old results that had these obsolete tests
|
|
if test == "rand_read_256K" or test == "rand_write_256K":
|
|
continue
|
|
|
|
ainformation.append("")
|
|
|
|
test_details = benchmark_details[test]
|
|
|
|
# Top row (Headers)
|
|
ainformation.append(
|
|
"{bold}\
|
|
{test_name: <{test_name_length}} \
|
|
{overall_label: <{overall_label_length}} \
|
|
{overall: <{overall_length}} \
|
|
{bandwidth_label: <{bandwidth_label_length}} \
|
|
{bandwidth: <{bandwidth_length}} \
|
|
{iops: <{iops_length}} \
|
|
{latency: <{latency_length}} \
|
|
{cpuutil_label: <{cpuutil_label_length}} \
|
|
{cpuutil: <{cpuutil_length}} \
|
|
{end_bold}".format(
|
|
bold=ansiprint.bold(),
|
|
end_bold=ansiprint.end(),
|
|
test_name="Test:",
|
|
test_name_length=test_name_length,
|
|
overall_label="",
|
|
overall_label_length=overall_label_length,
|
|
overall="General",
|
|
overall_length=overall_column_length,
|
|
bandwidth_label="",
|
|
bandwidth_label_length=bandwidth_label_length,
|
|
bandwidth="Bandwidth",
|
|
bandwidth_length=bandwidth_column_length,
|
|
iops="IOPS",
|
|
iops_length=iops_column_length,
|
|
latency="Latency (μs)",
|
|
latency_length=latency_column_length,
|
|
cpuutil_label="",
|
|
cpuutil_label_length=cpuutil_label_length,
|
|
cpuutil="CPU Util",
|
|
cpuutil_length=cpuutil_column_length,
|
|
)
|
|
)
|
|
# Second row (Test, Size, Min, User))
|
|
ainformation.append(
|
|
"{bold}\
|
|
{test_name: <{test_name_length}} \
|
|
{overall_label: >{overall_label_length}} \
|
|
{overall: <{overall_length}} \
|
|
{bandwidth_label: >{bandwidth_label_length}} \
|
|
{bandwidth: <{bandwidth_length}} \
|
|
{iops: <{iops_length}} \
|
|
{latency: <{latency_length}} \
|
|
{cpuutil_label: >{cpuutil_label_length}} \
|
|
{cpuutil: <{cpuutil_length}} \
|
|
{end_bold}".format(
|
|
bold="",
|
|
end_bold="",
|
|
test_name=nice_test_name_map[test],
|
|
test_name_length=test_name_length,
|
|
overall_label="Test Size:",
|
|
overall_label_length=overall_label_length,
|
|
overall=format_bytes_tohuman(
|
|
int(test_details["overall"]["iosize"]) * 1024
|
|
),
|
|
overall_length=overall_column_length,
|
|
bandwidth_label="Min:",
|
|
bandwidth_label_length=bandwidth_label_length,
|
|
bandwidth=format_bytes_tohuman(
|
|
int(test_details["bandwidth"]["min"]) * 1024
|
|
),
|
|
bandwidth_length=bandwidth_column_length,
|
|
iops=format_ops_tohuman(int(test_details["iops"]["min"])),
|
|
iops_length=iops_column_length,
|
|
latency=test_details["latency"]["min"],
|
|
latency_length=latency_column_length,
|
|
cpuutil_label="User:",
|
|
cpuutil_label_length=cpuutil_label_length,
|
|
cpuutil=test_details["cpu"]["user"],
|
|
cpuutil_length=cpuutil_column_length,
|
|
)
|
|
)
|
|
# Third row (blank, BW/s, Max, System))
|
|
ainformation.append(
|
|
"{bold}\
|
|
{test_name: <{test_name_length}} \
|
|
{overall_label: >{overall_label_length}} \
|
|
{overall: <{overall_length}} \
|
|
{bandwidth_label: >{bandwidth_label_length}} \
|
|
{bandwidth: <{bandwidth_length}} \
|
|
{iops: <{iops_length}} \
|
|
{latency: <{latency_length}} \
|
|
{cpuutil_label: >{cpuutil_label_length}} \
|
|
{cpuutil: <{cpuutil_length}} \
|
|
{end_bold}".format(
|
|
bold="",
|
|
end_bold="",
|
|
test_name="",
|
|
test_name_length=test_name_length,
|
|
overall_label="Bandwidth/s:",
|
|
overall_label_length=overall_label_length,
|
|
overall=format_bytes_tohuman(
|
|
int(test_details["overall"]["bandwidth"]) * 1024
|
|
),
|
|
overall_length=overall_column_length,
|
|
bandwidth_label="Max:",
|
|
bandwidth_label_length=bandwidth_label_length,
|
|
bandwidth=format_bytes_tohuman(
|
|
int(test_details["bandwidth"]["max"]) * 1024
|
|
),
|
|
bandwidth_length=bandwidth_column_length,
|
|
iops=format_ops_tohuman(int(test_details["iops"]["max"])),
|
|
iops_length=iops_column_length,
|
|
latency=test_details["latency"]["max"],
|
|
latency_length=latency_column_length,
|
|
cpuutil_label="System:",
|
|
cpuutil_label_length=cpuutil_label_length,
|
|
cpuutil=test_details["cpu"]["system"],
|
|
cpuutil_length=cpuutil_column_length,
|
|
)
|
|
)
|
|
# Fourth row (blank, IOPS, Mean, CtxSq))
|
|
ainformation.append(
|
|
"{bold}\
|
|
{test_name: <{test_name_length}} \
|
|
{overall_label: >{overall_label_length}} \
|
|
{overall: <{overall_length}} \
|
|
{bandwidth_label: >{bandwidth_label_length}} \
|
|
{bandwidth: <{bandwidth_length}} \
|
|
{iops: <{iops_length}} \
|
|
{latency: <{latency_length}} \
|
|
{cpuutil_label: >{cpuutil_label_length}} \
|
|
{cpuutil: <{cpuutil_length}} \
|
|
{end_bold}".format(
|
|
bold="",
|
|
end_bold="",
|
|
test_name="",
|
|
test_name_length=test_name_length,
|
|
overall_label="IOPS:",
|
|
overall_label_length=overall_label_length,
|
|
overall=format_ops_tohuman(int(test_details["overall"]["iops"])),
|
|
overall_length=overall_column_length,
|
|
bandwidth_label="Mean:",
|
|
bandwidth_label_length=bandwidth_label_length,
|
|
bandwidth=format_bytes_tohuman(
|
|
int(float(test_details["bandwidth"]["mean"])) * 1024
|
|
),
|
|
bandwidth_length=bandwidth_column_length,
|
|
iops=format_ops_tohuman(int(float(test_details["iops"]["mean"]))),
|
|
iops_length=iops_column_length,
|
|
latency=test_details["latency"]["mean"],
|
|
latency_length=latency_column_length,
|
|
cpuutil_label="CtxSw:",
|
|
cpuutil_label_length=cpuutil_label_length,
|
|
cpuutil=test_details["cpu"]["ctxsw"],
|
|
cpuutil_length=cpuutil_column_length,
|
|
)
|
|
)
|
|
# Fifth row (blank, Runtime, StdDev, MajFault))
|
|
ainformation.append(
|
|
"{bold}\
|
|
{test_name: <{test_name_length}} \
|
|
{overall_label: >{overall_label_length}} \
|
|
{overall: <{overall_length}} \
|
|
{bandwidth_label: >{bandwidth_label_length}} \
|
|
{bandwidth: <{bandwidth_length}} \
|
|
{iops: <{iops_length}} \
|
|
{latency: <{latency_length}} \
|
|
{cpuutil_label: >{cpuutil_label_length}} \
|
|
{cpuutil: <{cpuutil_length}} \
|
|
{end_bold}".format(
|
|
bold="",
|
|
end_bold="",
|
|
test_name="",
|
|
test_name_length=test_name_length,
|
|
overall_label="Runtime (s):",
|
|
overall_label_length=overall_label_length,
|
|
overall=int(test_details["overall"]["runtime"]) / 1000.0,
|
|
overall_length=overall_column_length,
|
|
bandwidth_label="StdDev:",
|
|
bandwidth_label_length=bandwidth_label_length,
|
|
bandwidth=format_bytes_tohuman(
|
|
int(float(test_details["bandwidth"]["stdev"])) * 1024
|
|
),
|
|
bandwidth_length=bandwidth_column_length,
|
|
iops=format_ops_tohuman(int(float(test_details["iops"]["stdev"]))),
|
|
iops_length=iops_column_length,
|
|
latency=test_details["latency"]["stdev"],
|
|
latency_length=latency_column_length,
|
|
cpuutil_label="MajFault:",
|
|
cpuutil_label_length=cpuutil_label_length,
|
|
cpuutil=test_details["cpu"]["majfault"],
|
|
cpuutil_length=cpuutil_column_length,
|
|
)
|
|
)
|
|
# Sixth row (blank, blank, Samples, MinFault))
|
|
ainformation.append(
|
|
"{bold}\
|
|
{test_name: <{test_name_length}} \
|
|
{overall_label: >{overall_label_length}} \
|
|
{overall: <{overall_length}} \
|
|
{bandwidth_label: >{bandwidth_label_length}} \
|
|
{bandwidth: <{bandwidth_length}} \
|
|
{iops: <{iops_length}} \
|
|
{latency: <{latency_length}} \
|
|
{cpuutil_label: >{cpuutil_label_length}} \
|
|
{cpuutil: <{cpuutil_length}} \
|
|
{end_bold}".format(
|
|
bold="",
|
|
end_bold="",
|
|
test_name="",
|
|
test_name_length=test_name_length,
|
|
overall_label="",
|
|
overall_label_length=overall_label_length,
|
|
overall="",
|
|
overall_length=overall_column_length,
|
|
bandwidth_label="Samples:",
|
|
bandwidth_label_length=bandwidth_label_length,
|
|
bandwidth=test_details["bandwidth"]["numsamples"],
|
|
bandwidth_length=bandwidth_column_length,
|
|
iops=test_details["iops"]["numsamples"],
|
|
iops_length=iops_column_length,
|
|
latency="",
|
|
latency_length=latency_column_length,
|
|
cpuutil_label="MinFault:",
|
|
cpuutil_label_length=cpuutil_label_length,
|
|
cpuutil=test_details["cpu"]["minfault"],
|
|
cpuutil_length=cpuutil_column_length,
|
|
)
|
|
)
|
|
|
|
ainformation.append("")
|
|
|
|
return "\n".join(ainformation)
|
|
|
|
|
|
def format_info_benchmark_json(config, benchmark_information):
|
|
if benchmark_information["benchmark_result"] == "Running":
|
|
return "Benchmark test is still running."
|
|
|
|
benchmark_details = benchmark_information["benchmark_result"]
|
|
|
|
# Format a nice output; do this line-by-line then concat the elements at the end
|
|
ainformation = []
|
|
ainformation.append(
|
|
"{}Storage Benchmark details:{}".format(ansiprint.bold(), ansiprint.end())
|
|
)
|
|
|
|
nice_test_name_map = {
|
|
"seq_read": "Sequential Read (4M blocks, queue depth 64)",
|
|
"seq_write": "Sequential Write (4M blocks, queue depth 64)",
|
|
"rand_read_4M": "Random Read (4M blocks, queue depth 64)",
|
|
"rand_write_4M": "Random Write (4M blocks queue depth 64)",
|
|
"rand_read_4K": "Random Read (4K blocks, queue depth 64)",
|
|
"rand_write_4K": "Random Write (4K blocks, queue depth 64)",
|
|
"rand_read_4K_lowdepth": "Random Read (4K blocks, queue depth 1)",
|
|
"rand_write_4K_lowdepth": "Random Write (4K blocks, queue depth 1)",
|
|
}
|
|
|
|
for test in benchmark_details:
|
|
ainformation.append("")
|
|
|
|
io_class = None
|
|
for _io_class in ["read", "write"]:
|
|
if benchmark_details[test]["jobs"][0][_io_class]["io_bytes"] > 0:
|
|
io_class = _io_class
|
|
if io_class is None:
|
|
continue
|
|
|
|
job_details = benchmark_details[test]["jobs"][0]
|
|
|
|
# Calculate the unified latency categories (in us)
|
|
latency_tree = list()
|
|
for field in job_details["latency_ns"]:
|
|
bucket = str(int(field) / 1000)
|
|
latency_tree.append((bucket, job_details["latency_ns"][field]))
|
|
for field in job_details["latency_us"]:
|
|
bucket = field
|
|
latency_tree.append((bucket, job_details["latency_us"][field]))
|
|
for field in job_details["latency_ms"]:
|
|
# That one annoying one
|
|
if field == ">=2000":
|
|
bucket = ">=2000000"
|
|
else:
|
|
bucket = str(int(field) * 1000)
|
|
latency_tree.append((bucket, job_details["latency_ms"][field]))
|
|
|
|
# Find the minimum entry without a zero
|
|
useful_latency_tree = list()
|
|
for element in latency_tree:
|
|
if element[1] != 0:
|
|
useful_latency_tree.append(element)
|
|
|
|
max_rows = 9
|
|
if len(useful_latency_tree) > 9:
|
|
max_rows = len(useful_latency_tree)
|
|
elif len(useful_latency_tree) < 9:
|
|
while len(useful_latency_tree) < 9:
|
|
useful_latency_tree.append(("", ""))
|
|
|
|
# Format the static data
|
|
overall_label = [
|
|
"Overall BW/s:",
|
|
"Overall IOPS:",
|
|
"Total I/O:",
|
|
"Runtime (s):",
|
|
"User CPU %:",
|
|
"System CPU %:",
|
|
"Ctx Switches:",
|
|
"Major Faults:",
|
|
"Minor Faults:",
|
|
]
|
|
while len(overall_label) < max_rows:
|
|
overall_label.append("")
|
|
|
|
overall_data = [
|
|
format_bytes_tohuman(int(job_details[io_class]["bw_bytes"])),
|
|
format_ops_tohuman(int(job_details[io_class]["iops"])),
|
|
format_bytes_tohuman(int(job_details[io_class]["io_bytes"])),
|
|
job_details["job_runtime"] / 1000,
|
|
job_details["usr_cpu"],
|
|
job_details["sys_cpu"],
|
|
job_details["ctx"],
|
|
job_details["majf"],
|
|
job_details["minf"],
|
|
]
|
|
while len(overall_data) < max_rows:
|
|
overall_data.append("")
|
|
|
|
bandwidth_label = [
|
|
"Min:",
|
|
"Max:",
|
|
"Mean:",
|
|
"StdDev:",
|
|
"Samples:",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
]
|
|
while len(bandwidth_label) < max_rows:
|
|
bandwidth_label.append("")
|
|
|
|
bandwidth_data = [
|
|
format_bytes_tohuman(int(job_details[io_class]["bw_min"]) * 1024),
|
|
format_bytes_tohuman(int(job_details[io_class]["bw_max"]) * 1024),
|
|
format_bytes_tohuman(int(job_details[io_class]["bw_mean"]) * 1024),
|
|
format_bytes_tohuman(int(job_details[io_class]["bw_dev"]) * 1024),
|
|
job_details[io_class]["bw_samples"],
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
]
|
|
while len(bandwidth_data) < max_rows:
|
|
bandwidth_data.append("")
|
|
|
|
iops_data = [
|
|
format_ops_tohuman(int(job_details[io_class]["iops_min"])),
|
|
format_ops_tohuman(int(job_details[io_class]["iops_max"])),
|
|
format_ops_tohuman(int(job_details[io_class]["iops_mean"])),
|
|
format_ops_tohuman(int(job_details[io_class]["iops_stddev"])),
|
|
job_details[io_class]["iops_samples"],
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
]
|
|
while len(iops_data) < max_rows:
|
|
iops_data.append("")
|
|
|
|
lat_data = [
|
|
int(job_details[io_class]["lat_ns"]["min"]) / 1000,
|
|
int(job_details[io_class]["lat_ns"]["max"]) / 1000,
|
|
int(job_details[io_class]["lat_ns"]["mean"]) / 1000,
|
|
int(job_details[io_class]["lat_ns"]["stddev"]) / 1000,
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
]
|
|
while len(lat_data) < max_rows:
|
|
lat_data.append("")
|
|
|
|
# Format the dynamic buckets
|
|
lat_bucket_label = list()
|
|
lat_bucket_data = list()
|
|
for element in useful_latency_tree:
|
|
lat_bucket_label.append(element[0])
|
|
lat_bucket_data.append(element[1])
|
|
|
|
# Column default widths
|
|
overall_label_length = 0
|
|
overall_column_length = 0
|
|
bandwidth_label_length = 0
|
|
bandwidth_column_length = 11
|
|
iops_column_length = 4
|
|
latency_column_length = 12
|
|
latency_bucket_label_length = 0
|
|
|
|
# Column layout:
|
|
# General Bandwidth IOPS Latency Percentiles
|
|
# --------- ---------- -------- -------- ---------------
|
|
# Size Min Min Min A
|
|
# BW Max Max Max B
|
|
# IOPS Mean Mean Mean ...
|
|
# Runtime StdDev StdDev StdDev Z
|
|
# UsrCPU Samples Samples
|
|
# SysCPU
|
|
# CtxSw
|
|
# MajFault
|
|
# MinFault
|
|
|
|
# Set column widths
|
|
for item in overall_label:
|
|
_item_length = len(str(item))
|
|
if _item_length > overall_label_length:
|
|
overall_label_length = _item_length
|
|
|
|
for item in overall_data:
|
|
_item_length = len(str(item))
|
|
if _item_length > overall_column_length:
|
|
overall_column_length = _item_length
|
|
|
|
test_name_length = len(nice_test_name_map[test])
|
|
if test_name_length > overall_label_length + overall_column_length:
|
|
_diff = test_name_length - (overall_label_length + overall_column_length)
|
|
overall_column_length += _diff
|
|
|
|
for item in bandwidth_label:
|
|
_item_length = len(str(item))
|
|
if _item_length > bandwidth_label_length:
|
|
bandwidth_label_length = _item_length
|
|
|
|
for item in bandwidth_data:
|
|
_item_length = len(str(item))
|
|
if _item_length > bandwidth_column_length:
|
|
bandwidth_column_length = _item_length
|
|
|
|
for item in iops_data:
|
|
_item_length = len(str(item))
|
|
if _item_length > iops_column_length:
|
|
iops_column_length = _item_length
|
|
|
|
for item in lat_data:
|
|
_item_length = len(str(item))
|
|
if _item_length > latency_column_length:
|
|
latency_column_length = _item_length
|
|
|
|
for item in lat_bucket_label:
|
|
_item_length = len(str(item))
|
|
if _item_length > latency_bucket_label_length:
|
|
latency_bucket_label_length = _item_length
|
|
|
|
# Top row (Headers)
|
|
ainformation.append(
|
|
"{bold}\
|
|
{overall_label: <{overall_label_length}} \
|
|
{bandwidth_label: <{bandwidth_label_length}} \
|
|
{bandwidth: <{bandwidth_length}} \
|
|
{iops: <{iops_length}} \
|
|
{latency: <{latency_length}} \
|
|
{latency_bucket_label: <{latency_bucket_label_length}} \
|
|
{latency_bucket} \
|
|
{end_bold}".format(
|
|
bold=ansiprint.bold(),
|
|
end_bold=ansiprint.end(),
|
|
overall_label=nice_test_name_map[test],
|
|
overall_label_length=overall_label_length,
|
|
bandwidth_label="",
|
|
bandwidth_label_length=bandwidth_label_length,
|
|
bandwidth="Bandwidth/s",
|
|
bandwidth_length=bandwidth_column_length,
|
|
iops="IOPS",
|
|
iops_length=iops_column_length,
|
|
latency="Latency (μs)",
|
|
latency_length=latency_column_length,
|
|
latency_bucket_label="Latency Buckets (μs/%)",
|
|
latency_bucket_label_length=latency_bucket_label_length,
|
|
latency_bucket="",
|
|
)
|
|
)
|
|
|
|
for idx in range(0, max_rows):
|
|
# Top row (Headers)
|
|
ainformation.append(
|
|
"{bold}\
|
|
{overall_label: >{overall_label_length}} \
|
|
{overall: <{overall_length}} \
|
|
{bandwidth_label: >{bandwidth_label_length}} \
|
|
{bandwidth: <{bandwidth_length}} \
|
|
{iops: <{iops_length}} \
|
|
{latency: <{latency_length}} \
|
|
{latency_bucket_label: >{latency_bucket_label_length}} \
|
|
{latency_bucket} \
|
|
{end_bold}".format(
|
|
bold="",
|
|
end_bold="",
|
|
overall_label=overall_label[idx],
|
|
overall_label_length=overall_label_length,
|
|
overall=overall_data[idx],
|
|
overall_length=overall_column_length,
|
|
bandwidth_label=bandwidth_label[idx],
|
|
bandwidth_label_length=bandwidth_label_length,
|
|
bandwidth=bandwidth_data[idx],
|
|
bandwidth_length=bandwidth_column_length,
|
|
iops=iops_data[idx],
|
|
iops_length=iops_column_length,
|
|
latency=lat_data[idx],
|
|
latency_length=latency_column_length,
|
|
latency_bucket_label=lat_bucket_label[idx],
|
|
latency_bucket_label_length=latency_bucket_label_length,
|
|
latency_bucket=lat_bucket_data[idx],
|
|
)
|
|
)
|
|
|
|
return "\n".join(ainformation)
|