Compare commits
2 Commits
master
...
1c69d2196b
Author | SHA1 | Date | |
---|---|---|---|
1c69d2196b | |||
768d435445 |
17
CHANGELOG.md
17
CHANGELOG.md
@ -1,22 +1,5 @@
|
||||
## PVC Changelog
|
||||
|
||||
###### [v1.0.1](https://github.com/parallelvirtualcluster/pvc/releases/tag/v1.0.1)
|
||||
|
||||
* [CLI Client] [Bugfix] Fix bug with DELETE endpoints returning invalid data
|
||||
|
||||
###### [v1.0](https://github.com/parallelvirtualcluster/pvc/releases/tag/v1.0)
|
||||
|
||||
**Announcement**: We are pleased to announce PVC 1.0! Functionally speaking, there are only a few minor improvements over the previous 0.9.107, but I believe it's finally time to call this a "1.0" release. Recently I have had much less opportunity to work on PVC as I would like, so some features are still not quite there, but those can arrive in future versions over time.
|
||||
**Enhancement**: The PVC CLI has been made much more efficient in terms of imports, allowing it to run on much lower spec hardware (in my case, on a small SBC). It's still not perfect, but multi-second import times are no longer an issue. The CLI client has also been moved to a more modern build system in preparation for Debian 13 "Trixie".
|
||||
|
||||
* [Daemons] Add cluster name to outputs during startup
|
||||
* [CLI Client] Translate domain UUIDs to names in full node detail output for better readability
|
||||
* [CLI Client] Fix colouring bug for mirror state
|
||||
* [CLI Client] Significantly improve import efficiency throughout the client to avoid long load times on slow hardware
|
||||
* [CLI Client] Port build to pyproject.toml and increase Debuild compat to 13
|
||||
* [API Daemon] Fix bug with RBD list update after VM rename
|
||||
* [API Daemon] Fix bug/crash if Ceph volume stats are invalid/empty
|
||||
|
||||
###### [v0.9.107](https://github.com/parallelvirtualcluster/pvc/releases/tag/v0.9.107)
|
||||
|
||||
* [Worker Daemon] Fixes a bug where snapshot removal fails during autobackups
|
||||
|
@ -22,8 +22,7 @@ sed -i "s,version = \"${current_version}\",version = \"${new_version}\"," node-d
|
||||
sed -i "s,version = \"${current_version}\",version = \"${new_version}\"," health-daemon/pvchealthd/Daemon.py
|
||||
sed -i "s,version = \"${current_version}\",version = \"${new_version}\"," worker-daemon/pvcworkerd/Daemon.py
|
||||
sed -i "s,version = \"${current_version}\",version = \"${new_version}\"," api-daemon/pvcapid/Daemon.py
|
||||
sed -i "s,version = \"${current_version}\",version = \"${new_version}\"," client-cli/pyproject.toml
|
||||
sed -i "s,VERSION = \"${current_version}\",VERSION = \"${new_version}\"," client-cli/pvc/cli/helpers.py
|
||||
sed -i "s,version=\"${current_version}\",version=\"${new_version}\"," client-cli/setup.py
|
||||
echo ${new_version} > .version
|
||||
|
||||
changelog_tmpdir=$( mktemp -d )
|
||||
@ -48,7 +47,7 @@ echo -e "${deb_changelog_new}" >> ${deb_changelog_file}
|
||||
echo -e "${deb_changelog_orig}" >> ${deb_changelog_file}
|
||||
mv ${deb_changelog_file} debian/changelog
|
||||
|
||||
git add node-daemon/pvcnoded/Daemon.py health-daemon/pvchealthd/Daemon.py worker-daemon/pvcworkerd/Daemon.py api-daemon/pvcapid/Daemon.py client-cli/pvc/cli/helpers.py client-cli/pyproject.toml debian/changelog CHANGELOG.md .version
|
||||
git add node-daemon/pvcnoded/Daemon.py health-daemon/pvchealthd/Daemon.py worker-daemon/pvcworkerd/Daemon.py api-daemon/pvcapid/Daemon.py client-cli/setup.py debian/changelog CHANGELOG.md .version
|
||||
git commit -v
|
||||
|
||||
popd &>/dev/null
|
||||
|
@ -1,5 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Import only the essential parts of click for CLI definition
|
||||
from click import group, command, option, argument, pass_context
|
||||
|
||||
# Import minimal required modules
|
||||
from pvc.lib.common import call_api
|
||||
|
||||
# Use lazy imports for modules not needed during initial CLI parsing
|
||||
from pvc.lib.lazy_imports import yaml, click_advanced
|
||||
|
||||
# cli.py - PVC Click CLI main library
|
||||
# Part of the Parallel Virtual Cluster (PVC) system
|
||||
#
|
||||
@ -97,7 +106,10 @@ def version(ctx, param, value):
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
|
||||
echo(CLI_CONFIG, f"Parallel Virtual Cluster CLI client version {VERSION}")
|
||||
from pkg_resources import get_distribution
|
||||
|
||||
version = get_distribution("pvc").version
|
||||
echo(CLI_CONFIG, f"Parallel Virtual Cluster CLI client version {version}")
|
||||
ctx.exit()
|
||||
|
||||
|
||||
|
@ -30,8 +30,6 @@ from yaml import load as yload
|
||||
from yaml import SafeLoader
|
||||
|
||||
|
||||
VERSION = "1.0.1"
|
||||
|
||||
DEFAULT_STORE_DATA = {"cfgfile": "/etc/pvc/pvc.conf"}
|
||||
DEFAULT_STORE_FILENAME = "pvc.json"
|
||||
DEFAULT_API_PREFIX = "/api/v1"
|
||||
|
@ -23,11 +23,336 @@ from ast import literal_eval
|
||||
from click import echo, progressbar
|
||||
from math import ceil
|
||||
from os.path import getsize
|
||||
from requests import get, post, put, patch, delete, Response
|
||||
from requests.exceptions import ConnectionError
|
||||
from time import time
|
||||
from urllib3 import disable_warnings
|
||||
from pvc.cli.helpers import VERSION
|
||||
import json
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
# Define a Response class to mimic requests.Response
|
||||
class Response:
|
||||
def __init__(self, status_code, headers, content):
|
||||
self.status_code = status_code
|
||||
self.headers = headers
|
||||
self.content = content
|
||||
self._json = None
|
||||
|
||||
def json(self):
|
||||
if self._json is None:
|
||||
try:
|
||||
self._json = json.loads(self.content.decode('utf-8'))
|
||||
except json.JSONDecodeError:
|
||||
self._json = {}
|
||||
return self._json
|
||||
|
||||
# Define ConnectionError to mimic requests.exceptions.ConnectionError
|
||||
class ConnectionError(Exception):
|
||||
pass
|
||||
|
||||
class ErrorResponse(Response):
|
||||
def __init__(self, json_data, status_code, headers):
|
||||
self.status_code = status_code
|
||||
self.headers = headers
|
||||
self._json = json_data
|
||||
self.content = json.dumps(json_data).encode('utf-8') if json_data else b''
|
||||
|
||||
def json(self):
|
||||
return self._json
|
||||
|
||||
def _encode_params(params):
|
||||
"""Simple URL parameter encoder"""
|
||||
if not params:
|
||||
return ''
|
||||
|
||||
parts = []
|
||||
for key, value in params.items():
|
||||
if isinstance(value, bool):
|
||||
value = str(value).lower()
|
||||
elif value is None:
|
||||
value = ''
|
||||
else:
|
||||
value = str(value)
|
||||
parts.append(f"{key}={value}")
|
||||
|
||||
return '?' + '&'.join(parts)
|
||||
|
||||
def call_api(
|
||||
config,
|
||||
operation,
|
||||
request_uri,
|
||||
headers={},
|
||||
params=None,
|
||||
data=None,
|
||||
files=None,
|
||||
):
|
||||
"""
|
||||
Make an API call to the PVC API using native Python libraries.
|
||||
"""
|
||||
# Set the connect timeout to 2 seconds
|
||||
timeout = 2.05
|
||||
|
||||
# Craft the URI
|
||||
uri = "{}://{}{}{}".format(
|
||||
config["api_scheme"], config["api_host"], config["api_prefix"], request_uri
|
||||
)
|
||||
|
||||
# Parse the URI without using urllib
|
||||
if '://' in uri:
|
||||
scheme, rest = uri.split('://', 1)
|
||||
else:
|
||||
scheme = 'http'
|
||||
rest = uri
|
||||
|
||||
if '/' in rest:
|
||||
netloc, path = rest.split('/', 1)
|
||||
path = '/' + path
|
||||
else:
|
||||
netloc = rest
|
||||
path = '/'
|
||||
|
||||
# Extract host and port
|
||||
if ':' in netloc:
|
||||
host, port_str = netloc.split(':', 1)
|
||||
port = int(port_str)
|
||||
else:
|
||||
host = netloc
|
||||
port = 443 if scheme == 'https' else 80
|
||||
|
||||
# Craft the authentication header if required
|
||||
if config["api_key"]:
|
||||
headers["X-Api-Key"] = config["api_key"]
|
||||
|
||||
# Add content type if not present
|
||||
if "Content-Type" not in headers and data is not None and files is None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
# Prepare query string
|
||||
query_string = _encode_params(params)
|
||||
|
||||
# Prepare path with query string
|
||||
full_path = path + query_string
|
||||
|
||||
# Prepare body
|
||||
body = None
|
||||
if data is not None and files is None:
|
||||
if isinstance(data, dict):
|
||||
body = json.dumps(data).encode('utf-8')
|
||||
else:
|
||||
body = data.encode('utf-8') if isinstance(data, str) else data
|
||||
|
||||
# Handle file uploads (multipart/form-data)
|
||||
if files:
|
||||
boundary = '----WebKitFormBoundary' + str(int(time()))
|
||||
headers['Content-Type'] = f'multipart/form-data; boundary={boundary}'
|
||||
|
||||
body = b''
|
||||
# Add form fields
|
||||
if data:
|
||||
for key, value in data.items():
|
||||
body += f'--{boundary}\r\n'.encode()
|
||||
body += f'Content-Disposition: form-data; name="{key}"\r\n\r\n'.encode()
|
||||
body += f'{value}\r\n'.encode()
|
||||
|
||||
# Add files
|
||||
for key, file_tuple in files.items():
|
||||
filename, fileobj, content_type = file_tuple
|
||||
body += f'--{boundary}\r\n'.encode()
|
||||
body += f'Content-Disposition: form-data; name="{key}"; filename="{filename}"\r\n'.encode()
|
||||
body += f'Content-Type: {content_type}\r\n\r\n'.encode()
|
||||
body += fileobj.read()
|
||||
body += b'\r\n'
|
||||
|
||||
body += f'--{boundary}--\r\n'.encode()
|
||||
|
||||
# Determine the request type and hit the API
|
||||
response = None
|
||||
|
||||
try:
|
||||
# Special handling for GET with retries
|
||||
if operation == "get":
|
||||
retry_on_code = [429, 500, 502, 503, 504]
|
||||
for i in range(3):
|
||||
failed = False
|
||||
try:
|
||||
# Create socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(timeout)
|
||||
|
||||
# Handle SSL for HTTPS
|
||||
if scheme == 'https':
|
||||
context = ssl.create_default_context()
|
||||
if not config["verify_ssl"]:
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
s = context.wrap_socket(s, server_hostname=host)
|
||||
|
||||
# Connect
|
||||
s.connect((host, port))
|
||||
|
||||
# Build request
|
||||
request = f"{operation.upper()} {full_path} HTTP/1.1\r\n"
|
||||
request += f"Host: {host}\r\n"
|
||||
|
||||
for key, value in headers.items():
|
||||
request += f"{key}: {value}\r\n"
|
||||
|
||||
if body:
|
||||
request += f"Content-Length: {len(body)}\r\n"
|
||||
|
||||
request += "Connection: close\r\n\r\n"
|
||||
|
||||
# Send request
|
||||
s.sendall(request.encode('utf-8'))
|
||||
if body:
|
||||
s.sendall(body)
|
||||
|
||||
# Read response
|
||||
response_data = b""
|
||||
while True:
|
||||
try:
|
||||
chunk = s.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
response_data += chunk
|
||||
except socket.timeout:
|
||||
# If we've received some data but timed out, that's okay
|
||||
if response_data:
|
||||
break
|
||||
else:
|
||||
raise
|
||||
|
||||
# Parse response
|
||||
if b'\r\n\r\n' in response_data:
|
||||
header_end = response_data.find(b'\r\n\r\n')
|
||||
headers_raw = response_data[:header_end].decode('utf-8')
|
||||
body_raw = response_data[header_end + 4:]
|
||||
|
||||
# Parse status code
|
||||
status_line = headers_raw.split('\r\n')[0]
|
||||
status_code = int(status_line.split(' ')[1])
|
||||
|
||||
# Parse headers
|
||||
headers_dict = {}
|
||||
for line in headers_raw.split('\r\n')[1:]:
|
||||
if not line:
|
||||
continue
|
||||
if ':' in line:
|
||||
key, value = line.split(':', 1)
|
||||
headers_dict[key.strip()] = value.strip()
|
||||
|
||||
# Create response object
|
||||
response = Response(status_code, headers_dict, body_raw)
|
||||
|
||||
if response.status_code in retry_on_code:
|
||||
failed = True
|
||||
s.close()
|
||||
continue
|
||||
break
|
||||
else:
|
||||
failed = True
|
||||
s.close()
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
failed = True
|
||||
if 's' in locals():
|
||||
s.close()
|
||||
continue
|
||||
finally:
|
||||
if 's' in locals():
|
||||
s.close()
|
||||
|
||||
if failed:
|
||||
error = f"Code {response.status_code}" if response else "Timeout"
|
||||
raise ConnectionError(f"Failed to connect after 3 tries ({error})")
|
||||
else:
|
||||
# Create socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(timeout)
|
||||
|
||||
try:
|
||||
# Handle SSL for HTTPS
|
||||
if scheme == 'https':
|
||||
context = ssl.create_default_context()
|
||||
if not config["verify_ssl"]:
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
s = context.wrap_socket(s, server_hostname=host)
|
||||
|
||||
# Connect
|
||||
s.connect((host, port))
|
||||
|
||||
# Build request
|
||||
request = f"{operation.upper()} {full_path} HTTP/1.1\r\n"
|
||||
request += f"Host: {host}\r\n"
|
||||
|
||||
for key, value in headers.items():
|
||||
request += f"{key}: {value}\r\n"
|
||||
|
||||
if body:
|
||||
request += f"Content-Length: {len(body)}\r\n"
|
||||
|
||||
request += "Connection: close\r\n\r\n"
|
||||
|
||||
# Send request
|
||||
s.sendall(request.encode('utf-8'))
|
||||
if body:
|
||||
s.sendall(body)
|
||||
|
||||
# Read response
|
||||
response_data = b""
|
||||
while True:
|
||||
try:
|
||||
chunk = s.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
response_data += chunk
|
||||
except socket.timeout:
|
||||
# If we've received some data but timed out, that's okay
|
||||
if response_data:
|
||||
break
|
||||
else:
|
||||
raise
|
||||
|
||||
# Parse response
|
||||
if b'\r\n\r\n' in response_data:
|
||||
header_end = response_data.find(b'\r\n\r\n')
|
||||
headers_raw = response_data[:header_end].decode('utf-8')
|
||||
body_raw = response_data[header_end + 4:]
|
||||
|
||||
# Parse status code
|
||||
status_line = headers_raw.split('\r\n')[0]
|
||||
status_code = int(status_line.split(' ')[1])
|
||||
|
||||
# Parse headers
|
||||
headers_dict = {}
|
||||
for line in headers_raw.split('\r\n')[1:]:
|
||||
if not line:
|
||||
continue
|
||||
if ':' in line:
|
||||
key, value = line.split(':', 1)
|
||||
headers_dict[key.strip()] = value.strip()
|
||||
|
||||
# Create response object
|
||||
response = Response(status_code, headers_dict, body_raw)
|
||||
else:
|
||||
raise Exception("Invalid HTTP response")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
except Exception as e:
|
||||
message = f"Failed to connect to the API: {e}"
|
||||
code = getattr(response, 'status_code', 504)
|
||||
response = ErrorResponse({"message": message}, code, None)
|
||||
|
||||
# Display debug output
|
||||
if config["debug"]:
|
||||
echo("API endpoint: {}".format(uri), err=True)
|
||||
echo("Response code: {}".format(response.status_code), err=True)
|
||||
echo("Response headers: {}".format(response.headers), err=True)
|
||||
echo(err=True)
|
||||
|
||||
# Return the response object
|
||||
return response
|
||||
|
||||
|
||||
def format_bytes(size_bytes):
|
||||
@ -140,121 +465,6 @@ class UploadProgressBar(object):
|
||||
echo(self.end_message + self.end_suffix, nl=self.end_nl)
|
||||
|
||||
|
||||
class ErrorResponse(Response):
|
||||
def __init__(self, json_data, status_code, headers):
|
||||
self.json_data = json_data
|
||||
self.status_code = status_code
|
||||
self.headers = headers
|
||||
|
||||
def json(self):
|
||||
return self.json_data
|
||||
|
||||
|
||||
def call_api(
|
||||
config,
|
||||
operation,
|
||||
request_uri,
|
||||
headers={},
|
||||
params=None,
|
||||
data=None,
|
||||
files=None,
|
||||
):
|
||||
# Set the connect timeout to 2 seconds but extremely long (48 hour) data timeout
|
||||
timeout = (2.05, 172800)
|
||||
|
||||
# Craft the URI
|
||||
uri = "{}://{}{}{}".format(
|
||||
config["api_scheme"], config["api_host"], config["api_prefix"], request_uri
|
||||
)
|
||||
|
||||
# Add custom User-Agent header
|
||||
headers["User-Agent"] = f"pvc-client-cli/{VERSION}"
|
||||
|
||||
# Craft the authentication header if required
|
||||
if config["api_key"]:
|
||||
headers["X-Api-Key"] = config["api_key"]
|
||||
|
||||
# Determine the request type and hit the API
|
||||
disable_warnings()
|
||||
try:
|
||||
response = None
|
||||
if operation == "get":
|
||||
retry_on_code = [429, 500, 502, 503, 504]
|
||||
for i in range(3):
|
||||
failed = False
|
||||
try:
|
||||
response = get(
|
||||
uri,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
params=params,
|
||||
data=data,
|
||||
verify=config["verify_ssl"],
|
||||
)
|
||||
if response.status_code in retry_on_code:
|
||||
failed = True
|
||||
continue
|
||||
break
|
||||
except ConnectionError:
|
||||
failed = True
|
||||
continue
|
||||
if failed:
|
||||
error = f"Code {response.status_code}" if response else "Timeout"
|
||||
raise ConnectionError(f"Failed to connect after 3 tries ({error})")
|
||||
if operation == "post":
|
||||
response = post(
|
||||
uri,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
params=params,
|
||||
data=data,
|
||||
files=files,
|
||||
verify=config["verify_ssl"],
|
||||
)
|
||||
if operation == "put":
|
||||
response = put(
|
||||
uri,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
params=params,
|
||||
data=data,
|
||||
files=files,
|
||||
verify=config["verify_ssl"],
|
||||
)
|
||||
if operation == "patch":
|
||||
response = patch(
|
||||
uri,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
params=params,
|
||||
data=data,
|
||||
verify=config["verify_ssl"],
|
||||
)
|
||||
if operation == "delete":
|
||||
response = delete(
|
||||
uri,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
params=params,
|
||||
data=data,
|
||||
verify=config["verify_ssl"],
|
||||
)
|
||||
except Exception as e:
|
||||
message = "Failed to connect to the API: {}".format(e)
|
||||
code = response.status_code if response else 504
|
||||
response = ErrorResponse({"message": message}, code, None)
|
||||
|
||||
# Display debug output
|
||||
if config["debug"]:
|
||||
echo("API endpoint: {}".format(uri), err=True)
|
||||
echo("Response code: {}".format(response.status_code), err=True)
|
||||
echo("Response headers: {}".format(response.headers), err=True)
|
||||
echo(err=True)
|
||||
|
||||
# Return the response object
|
||||
return response
|
||||
|
||||
|
||||
def get_wait_retdata(response, wait_flag):
|
||||
if response.status_code == 202:
|
||||
retvalue = True
|
||||
|
21
client-cli/pvc/lib/lazy_imports.py
Normal file
21
client-cli/pvc/lib/lazy_imports.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""
|
||||
Lazy import mechanism for PVC CLI to reduce startup time
|
||||
"""
|
||||
|
||||
class LazyModule:
|
||||
"""
|
||||
A proxy for a module that is loaded only when actually used
|
||||
"""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self._module = None
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if self._module is None:
|
||||
import importlib
|
||||
self._module = importlib.import_module(self.name)
|
||||
return getattr(self._module, attr)
|
||||
|
||||
# Create lazy module proxies
|
||||
yaml = LazyModule('yaml')
|
||||
click_advanced = LazyModule('click') # For advanced click features not used at startup
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pvc"
|
||||
version = "1.0.1"
|
||||
version = "0.9.107"
|
||||
dependencies = [
|
||||
"Click",
|
||||
"PyYAML",
|
||||
|
@ -555,16 +555,9 @@ def getCephVolumes(zkhandler, pool):
|
||||
def getVolumeInformation(zkhandler, pool, volume):
|
||||
# Parse the stats data
|
||||
volume_stats_raw = zkhandler.read(("volume.stats", f"{pool}/{volume}"))
|
||||
try:
|
||||
volume_stats = dict(json.loads(volume_stats_raw))
|
||||
# Format the size to something nicer
|
||||
volume_stats["size"] = format_bytes_tohuman(volume_stats["size"])
|
||||
except Exception:
|
||||
volume_stats = dict(
|
||||
json.loads(
|
||||
f'{"name":"{volume}","id":"","size":0,"objects":0,"order":0,"object_size":0,"snapshot_count":0,"block_name_prefix":"","format":0,"features":[],"op_features":[],"flags":[],"create_timestamp":"","access_timestamp":"","modify_timestamp":""}'
|
||||
)
|
||||
)
|
||||
volume_stats = dict(json.loads(volume_stats_raw))
|
||||
# Format the size to something nicer
|
||||
volume_stats["size"] = format_bytes_tohuman(volume_stats["size"])
|
||||
|
||||
volume_information = {"name": volume, "pool": pool, "stats": volume_stats}
|
||||
return volume_information
|
||||
|
@ -605,14 +605,12 @@ def rename_vm(zkhandler, domain, new_domain):
|
||||
rbd_list.append(disk["name"].split("/")[1])
|
||||
|
||||
# Rename each volume in turn
|
||||
rbd_list_new = []
|
||||
for idx, rbd in enumerate(rbd_list):
|
||||
rbd_new = re.sub(r"{}".format(domain), new_domain, rbd)
|
||||
# Skip renaming if nothing changed
|
||||
if rbd_new == rbd:
|
||||
continue
|
||||
ceph.rename_volume(zkhandler, pool_list[idx], rbd, rbd_new)
|
||||
rbd_list_new.append(f"{pool_list[idx]}/{rbd_new}")
|
||||
|
||||
# Replace the name in the config
|
||||
vm_config_new = (
|
||||
@ -629,7 +627,6 @@ def rename_vm(zkhandler, domain, new_domain):
|
||||
[
|
||||
(("domain", dom_uuid), new_domain),
|
||||
(("domain.xml", dom_uuid), vm_config_new),
|
||||
(("domain.rbdlist", dom_uuid), ",".join(rbd_list_new)),
|
||||
]
|
||||
)
|
||||
|
||||
|
21
debian/changelog
vendored
21
debian/changelog
vendored
@ -1,24 +1,3 @@
|
||||
pvc (1.0.1-0) unstable; urgency=high
|
||||
|
||||
* [CLI Client] [Bugfix] Fix bug with DELETE endpoints returning invalid data
|
||||
|
||||
-- Joshua M. Boniface <joshua@boniface.me> Sat, 21 Jun 2025 12:40:33 -0400
|
||||
|
||||
pvc (1.0-0) unstable; urgency=high
|
||||
|
||||
**Announcement**: We are pleased to announce PVC 1.0! Functionally speaking, there are only a few minor improvements over the previous 0.9.107, but I believe it's finally time to call this a "1.0" release. Recently I have had much less opportunity to work on PVC as I would like, so some features are still not quite there, but those can arrive in future versions over time.
|
||||
**Enhancement**: The PVC CLI has been made much more efficient in terms of imports, allowing it to run on much lower spec hardware (in my case, on a small SBC). It's still not perfect, but multi-second import times are no longer an issue. The CLI client has also been moved to a more modern build system in preparation for Debian 13 "Trixie".
|
||||
|
||||
* [Daemons] Add cluster name to outputs during startup
|
||||
* [CLI Client] Translate domain UUIDs to names in full node detail output for better readability
|
||||
* [CLI Client] Fix colouring bug for mirror state
|
||||
* [CLI Client] Significantly improve import efficiency throughout the client to avoid long load times on slow hardware
|
||||
* [CLI Client] Port build to pyproject.toml and increase Debuild compat to 13
|
||||
* [API Daemon] Fix bug with RBD list update after VM rename
|
||||
* [API Daemon] Fix bug/crash if Ceph volume stats are invalid/empty
|
||||
|
||||
-- Joshua M. Boniface <joshua@boniface.me> Thu, 05 Jun 2025 00:04:54 -0400
|
||||
|
||||
pvc (0.9.107-0) unstable; urgency=high
|
||||
|
||||
* [Worker Daemon] Fixes a bug where snapshot removal fails during autobackups
|
||||
|
@ -33,7 +33,7 @@ import os
|
||||
import signal
|
||||
|
||||
# Daemon version
|
||||
version = "1.0.1"
|
||||
version = "0.9.107"
|
||||
|
||||
|
||||
##########################################################
|
||||
|
@ -49,7 +49,7 @@ import re
|
||||
import json
|
||||
|
||||
# Daemon version
|
||||
version = "1.0.1"
|
||||
version = "0.9.107"
|
||||
|
||||
|
||||
##########################################################
|
||||
|
@ -58,7 +58,7 @@ from daemon_lib.automirror import (
|
||||
)
|
||||
|
||||
# Daemon version
|
||||
version = "1.0.1"
|
||||
version = "0.9.107"
|
||||
|
||||
|
||||
config = cfg.get_configuration()
|
||||
|
Reference in New Issue
Block a user