Compare commits
7 Commits
60967b5606
...
client-spe
Author | SHA1 | Date | |
---|---|---|---|
|
2eb0c3944e | ||
|
768d435445 | ||
bb77c5f1fc | |||
fc740927cc | |||
34149fe933 | |||
a2fed1885c | |||
ee055bdb81 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ debian/pvc-*/
|
|||||||
debian/*.log
|
debian/*.log
|
||||||
debian/*.substvars
|
debian/*.substvars
|
||||||
debian/files
|
debian/files
|
||||||
|
client-cli/build/
|
||||||
|
@@ -22,7 +22,8 @@ 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}\"," 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}\"," 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}\"," api-daemon/pvcapid/Daemon.py
|
||||||
sed -i "s,version=\"${current_version}\",version=\"${new_version}\"," client-cli/setup.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/cli.py
|
||||||
echo ${new_version} > .version
|
echo ${new_version} > .version
|
||||||
|
|
||||||
changelog_tmpdir=$( mktemp -d )
|
changelog_tmpdir=$( mktemp -d )
|
||||||
|
@@ -19,13 +19,23 @@
|
|||||||
#
|
#
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
VERSION = "0.9.107"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from difflib import unified_diff
|
from difflib import unified_diff
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from json import dump as jdump
|
from json import dump as jdump
|
||||||
from json import dumps as jdumps
|
from json import dumps as jdumps
|
||||||
from json import loads as jloads
|
from json import loads as jloads
|
||||||
from lxml.etree import fromstring, tostring
|
|
||||||
from os import environ, makedirs, path
|
from os import environ, makedirs, path
|
||||||
from re import sub, match
|
from re import sub, match
|
||||||
from yaml import load as yload
|
from yaml import load as yload
|
||||||
@@ -98,10 +108,7 @@ def version(ctx, param, value):
|
|||||||
if not value or ctx.resilient_parsing:
|
if not value or ctx.resilient_parsing:
|
||||||
return
|
return
|
||||||
|
|
||||||
from pkg_resources import get_distribution
|
echo(CLI_CONFIG, f"Parallel Virtual Cluster CLI client version {VERSION}")
|
||||||
|
|
||||||
version = get_distribution("pvc").version
|
|
||||||
echo(CLI_CONFIG, f"Parallel Virtual Cluster CLI client version {version}")
|
|
||||||
ctx.exit()
|
ctx.exit()
|
||||||
|
|
||||||
|
|
||||||
@@ -1192,6 +1199,8 @@ def cli_vm_define(
|
|||||||
|
|
||||||
# Verify our XML is sensible
|
# Verify our XML is sensible
|
||||||
try:
|
try:
|
||||||
|
from lxml.etree import fromstring, tostring
|
||||||
|
|
||||||
xml_data = fromstring(vmconfig_data)
|
xml_data = fromstring(vmconfig_data)
|
||||||
new_cfg = tostring(xml_data, pretty_print=True).decode("utf8")
|
new_cfg = tostring(xml_data, pretty_print=True).decode("utf8")
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1377,6 +1386,9 @@ def cli_vm_modify(
|
|||||||
|
|
||||||
# Grab the current config
|
# Grab the current config
|
||||||
current_vm_cfg_raw = vm_information.get("xml")
|
current_vm_cfg_raw = vm_information.get("xml")
|
||||||
|
|
||||||
|
from lxml.etree import fromstring, tostring
|
||||||
|
|
||||||
xml_data = fromstring(current_vm_cfg_raw)
|
xml_data = fromstring(current_vm_cfg_raw)
|
||||||
current_vm_cfgfile = tostring(xml_data, pretty_print=True).decode("utf8").strip()
|
current_vm_cfgfile = tostring(xml_data, pretty_print=True).decode("utf8").strip()
|
||||||
|
|
||||||
@@ -1435,6 +1447,8 @@ def cli_vm_modify(
|
|||||||
|
|
||||||
# Verify our XML is sensible
|
# Verify our XML is sensible
|
||||||
try:
|
try:
|
||||||
|
from lxml.etree import fromstring, tostring
|
||||||
|
|
||||||
xml_data = fromstring(new_vm_cfgfile)
|
xml_data = fromstring(new_vm_cfgfile)
|
||||||
new_cfg = tostring(xml_data, pretty_print=True).decode("utf8")
|
new_cfg = tostring(xml_data, pretty_print=True).decode("utf8")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -3265,6 +3279,9 @@ def cli_vm_dump(filename, domain):
|
|||||||
finish(False, 'ERROR: Could not find VM "{}"!'.format(domain))
|
finish(False, 'ERROR: Could not find VM "{}"!'.format(domain))
|
||||||
|
|
||||||
current_vm_cfg_raw = retdata.get("xml")
|
current_vm_cfg_raw = retdata.get("xml")
|
||||||
|
|
||||||
|
from lxml.etree import fromstring, tostring
|
||||||
|
|
||||||
xml_data = fromstring(current_vm_cfg_raw)
|
xml_data = fromstring(current_vm_cfg_raw)
|
||||||
current_vm_cfgfile = tostring(xml_data, pretty_print=True).decode("utf8")
|
current_vm_cfgfile = tostring(xml_data, pretty_print=True).decode("utf8")
|
||||||
xml = current_vm_cfgfile.strip()
|
xml = current_vm_cfgfile.strip()
|
||||||
|
@@ -20,7 +20,6 @@
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
from click import echo as click_echo
|
from click import echo as click_echo
|
||||||
from distutils.util import strtobool
|
|
||||||
from json import load as jload
|
from json import load as jload
|
||||||
from json import dump as jdump
|
from json import dump as jdump
|
||||||
from os import chmod, environ, getpid, path, get_terminal_size
|
from os import chmod, environ, getpid, path, get_terminal_size
|
||||||
@@ -150,9 +149,7 @@ def get_config(store_data, connection=None):
|
|||||||
if connection == "local":
|
if connection == "local":
|
||||||
config["verify_ssl"] = False
|
config["verify_ssl"] = False
|
||||||
else:
|
else:
|
||||||
config["verify_ssl"] = bool(
|
config["verify_ssl"] = environ.get("PVC_CLIENT_VERIFY_SSL", "True") == "True"
|
||||||
strtobool(environ.get("PVC_CLIENT_VERIFY_SSL", "True"))
|
|
||||||
)
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
@@ -19,13 +19,289 @@
|
|||||||
#
|
#
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
import os
|
|
||||||
import math
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
import click
|
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from urllib3 import disable_warnings
|
from click import echo, progressbar
|
||||||
|
from math import ceil
|
||||||
|
from os.path import getsize
|
||||||
|
from time import time
|
||||||
|
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
|
||||||
|
self.text = content.decode('utf-8', errors='replace') if content else ''
|
||||||
|
self.ok = 200 <= status_code < 300
|
||||||
|
self.url = None # Will be set by call_api
|
||||||
|
self.reason = None # HTTP reason phrase
|
||||||
|
self.encoding = 'utf-8'
|
||||||
|
self.elapsed = None # Time elapsed
|
||||||
|
self.request = None # Original request
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
if self._json is None:
|
||||||
|
# Don't catch JSONDecodeError - let it propagate like requests does
|
||||||
|
self._json = json.loads(self.content.decode('utf-8'))
|
||||||
|
return self._json
|
||||||
|
|
||||||
|
def raise_for_status(self):
|
||||||
|
"""Raises HTTPError if the status code is 4XX/5XX"""
|
||||||
|
if 400 <= self.status_code < 600:
|
||||||
|
raise Exception(f"HTTP Error {self.status_code}")
|
||||||
|
|
||||||
|
# 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 = ''
|
||||||
|
elif isinstance(value, (list, tuple)):
|
||||||
|
# Handle lists and tuples by creating multiple parameters with the same name
|
||||||
|
for item in value:
|
||||||
|
parts.append(f"{key}={item}")
|
||||||
|
continue # Skip the normal append since we've already added the parts
|
||||||
|
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 timeouts - fast connection timeout, longer read timeout
|
||||||
|
connect_timeout = 2.05 # Connection timeout in seconds
|
||||||
|
read_timeout = 172800.0 # Read timeout in seconds (much longer to allow for slow operations)
|
||||||
|
|
||||||
|
# Import VERSION from cli.py - this is a lightweight import since cli.py is already loaded
|
||||||
|
from pvc.cli.cli import VERSION
|
||||||
|
|
||||||
|
# Set User-Agent header if not already set
|
||||||
|
if "User-Agent" not in headers:
|
||||||
|
headers["User-Agent"] = f"pvc-client-cli/{VERSION}"
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
# Use http.client instead of raw sockets for better HTTP protocol handling
|
||||||
|
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 the appropriate connection with separate timeouts
|
||||||
|
if scheme == 'https':
|
||||||
|
import ssl
|
||||||
|
context = None
|
||||||
|
if not config["verify_ssl"]:
|
||||||
|
context = ssl._create_unverified_context()
|
||||||
|
|
||||||
|
import http.client
|
||||||
|
conn = http.client.HTTPSConnection(
|
||||||
|
host,
|
||||||
|
port=port,
|
||||||
|
timeout=connect_timeout, # This is the connection timeout
|
||||||
|
context=context
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
import http.client
|
||||||
|
conn = http.client.HTTPConnection(
|
||||||
|
host,
|
||||||
|
port=port,
|
||||||
|
timeout=connect_timeout # This is the connection timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the request
|
||||||
|
conn.request(operation.upper(), full_path, body=body, headers=headers)
|
||||||
|
|
||||||
|
# Set a longer timeout for reading the response
|
||||||
|
conn.sock.settimeout(read_timeout)
|
||||||
|
|
||||||
|
http_response = conn.getresponse()
|
||||||
|
|
||||||
|
# Read response data
|
||||||
|
status_code = http_response.status
|
||||||
|
response_headers = dict(http_response.getheaders())
|
||||||
|
response_data = http_response.read()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Create a Response object
|
||||||
|
response = Response(status_code, response_headers, response_data)
|
||||||
|
|
||||||
|
if response.status_code in retry_on_code:
|
||||||
|
failed = True
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
failed = True
|
||||||
|
if 'conn' in locals():
|
||||||
|
conn.close()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
error = f"Code {response.status_code}" if 'response' in locals() else "Timeout"
|
||||||
|
raise ConnectionError(f"Failed to connect after 3 tries ({error})")
|
||||||
|
else:
|
||||||
|
# Handle other HTTP methods
|
||||||
|
if scheme == 'https':
|
||||||
|
import ssl
|
||||||
|
context = None
|
||||||
|
if not config["verify_ssl"]:
|
||||||
|
context = ssl._create_unverified_context()
|
||||||
|
|
||||||
|
import http.client
|
||||||
|
conn = http.client.HTTPSConnection(
|
||||||
|
host,
|
||||||
|
port=port,
|
||||||
|
timeout=connect_timeout, # This is the connection timeout
|
||||||
|
context=context
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
import http.client
|
||||||
|
conn = http.client.HTTPConnection(
|
||||||
|
host,
|
||||||
|
port=port,
|
||||||
|
timeout=connect_timeout # This is the connection timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make the request
|
||||||
|
conn.request(operation.upper(), full_path, body=body, headers=headers)
|
||||||
|
|
||||||
|
# Set a longer timeout for reading the response
|
||||||
|
conn.sock.settimeout(read_timeout)
|
||||||
|
|
||||||
|
http_response = conn.getresponse()
|
||||||
|
|
||||||
|
# Read response data
|
||||||
|
status_code = http_response.status
|
||||||
|
response_headers = dict(http_response.getheaders())
|
||||||
|
response_data = http_response.read()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Create a Response object
|
||||||
|
response = Response(status_code, response_headers, response_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
message = f"Failed to connect to the API: {e}"
|
||||||
|
code = getattr(response, 'status_code', 504) if 'response' in locals() 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)
|
||||||
|
|
||||||
|
# Always return the response object - no special handling
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def format_bytes(size_bytes):
|
def format_bytes(size_bytes):
|
||||||
@@ -39,7 +315,7 @@ def format_bytes(size_bytes):
|
|||||||
}
|
}
|
||||||
human_bytes = "0B"
|
human_bytes = "0B"
|
||||||
for unit in sorted(byte_unit_matrix, key=byte_unit_matrix.get):
|
for unit in sorted(byte_unit_matrix, key=byte_unit_matrix.get):
|
||||||
formatted_bytes = int(math.ceil(size_bytes / byte_unit_matrix[unit]))
|
formatted_bytes = int(ceil(size_bytes / byte_unit_matrix[unit]))
|
||||||
if formatted_bytes < 10000:
|
if formatted_bytes < 10000:
|
||||||
human_bytes = "{}{}".format(formatted_bytes, unit)
|
human_bytes = "{}{}".format(formatted_bytes, unit)
|
||||||
break
|
break
|
||||||
@@ -57,7 +333,7 @@ def format_metric(integer):
|
|||||||
}
|
}
|
||||||
human_integer = "0"
|
human_integer = "0"
|
||||||
for unit in sorted(integer_unit_matrix, key=integer_unit_matrix.get):
|
for unit in sorted(integer_unit_matrix, key=integer_unit_matrix.get):
|
||||||
formatted_integer = int(math.ceil(integer / integer_unit_matrix[unit]))
|
formatted_integer = int(ceil(integer / integer_unit_matrix[unit]))
|
||||||
if formatted_integer < 10000:
|
if formatted_integer < 10000:
|
||||||
human_integer = "{}{}".format(formatted_integer, unit)
|
human_integer = "{}{}".format(formatted_integer, unit)
|
||||||
break
|
break
|
||||||
@@ -97,12 +373,12 @@ def format_age(age_secs):
|
|||||||
|
|
||||||
class UploadProgressBar(object):
|
class UploadProgressBar(object):
|
||||||
def __init__(self, filename, end_message="", end_nl=True):
|
def __init__(self, filename, end_message="", end_nl=True):
|
||||||
file_size = os.path.getsize(filename)
|
file_size = getsize(filename)
|
||||||
file_size_human = format_bytes(file_size)
|
file_size_human = format_bytes(file_size)
|
||||||
click.echo("Uploading file (total size {})...".format(file_size_human))
|
echo("Uploading file (total size {})...".format(file_size_human))
|
||||||
|
|
||||||
self.length = file_size
|
self.length = file_size
|
||||||
self.time_last = int(round(time.time() * 1000)) - 1000
|
self.time_last = int(round(time() * 1000)) - 1000
|
||||||
self.bytes_last = 0
|
self.bytes_last = 0
|
||||||
self.bytes_diff = 0
|
self.bytes_diff = 0
|
||||||
self.is_end = False
|
self.is_end = False
|
||||||
@@ -114,7 +390,7 @@ class UploadProgressBar(object):
|
|||||||
else:
|
else:
|
||||||
self.end_suffix = ""
|
self.end_suffix = ""
|
||||||
|
|
||||||
self.bar = click.progressbar(length=self.length, width=20, show_eta=True)
|
self.bar = progressbar(length=self.length, width=20, show_eta=True)
|
||||||
|
|
||||||
def update(self, monitor):
|
def update(self, monitor):
|
||||||
bytes_cur = monitor.bytes_read
|
bytes_cur = monitor.bytes_read
|
||||||
@@ -123,7 +399,7 @@ class UploadProgressBar(object):
|
|||||||
self.is_end = True
|
self.is_end = True
|
||||||
self.bytes_last = bytes_cur
|
self.bytes_last = bytes_cur
|
||||||
|
|
||||||
time_cur = int(round(time.time() * 1000))
|
time_cur = int(round(time() * 1000))
|
||||||
if (time_cur - 1000) > self.time_last:
|
if (time_cur - 1000) > self.time_last:
|
||||||
self.time_last = time_cur
|
self.time_last = time_cur
|
||||||
self.bar.update(self.bytes_diff)
|
self.bar.update(self.bytes_diff)
|
||||||
@@ -132,124 +408,10 @@ class UploadProgressBar(object):
|
|||||||
if self.is_end:
|
if self.is_end:
|
||||||
self.bar.update(self.bytes_diff)
|
self.bar.update(self.bytes_diff)
|
||||||
self.bytes_diff = 0
|
self.bytes_diff = 0
|
||||||
click.echo()
|
echo()
|
||||||
click.echo()
|
echo()
|
||||||
if self.end_message:
|
if self.end_message:
|
||||||
click.echo(self.end_message + self.end_suffix, nl=self.end_nl)
|
echo(self.end_message + self.end_suffix, nl=self.end_nl)
|
||||||
|
|
||||||
|
|
||||||
class ErrorResponse(requests.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
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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 = requests.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 requests.exceptions.ConnectionError:
|
|
||||||
failed = True
|
|
||||||
continue
|
|
||||||
if failed:
|
|
||||||
error = f"Code {response.status_code}" if response else "Timeout"
|
|
||||||
raise requests.exceptions.ConnectionError(
|
|
||||||
f"Failed to connect after 3 tries ({error})"
|
|
||||||
)
|
|
||||||
if operation == "post":
|
|
||||||
response = requests.post(
|
|
||||||
uri,
|
|
||||||
timeout=timeout,
|
|
||||||
headers=headers,
|
|
||||||
params=params,
|
|
||||||
data=data,
|
|
||||||
files=files,
|
|
||||||
verify=config["verify_ssl"],
|
|
||||||
)
|
|
||||||
if operation == "put":
|
|
||||||
response = requests.put(
|
|
||||||
uri,
|
|
||||||
timeout=timeout,
|
|
||||||
headers=headers,
|
|
||||||
params=params,
|
|
||||||
data=data,
|
|
||||||
files=files,
|
|
||||||
verify=config["verify_ssl"],
|
|
||||||
)
|
|
||||||
if operation == "patch":
|
|
||||||
response = requests.patch(
|
|
||||||
uri,
|
|
||||||
timeout=timeout,
|
|
||||||
headers=headers,
|
|
||||||
params=params,
|
|
||||||
data=data,
|
|
||||||
verify=config["verify_ssl"],
|
|
||||||
)
|
|
||||||
if operation == "delete":
|
|
||||||
response = requests.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"]:
|
|
||||||
click.echo("API endpoint: {}".format(uri), err=True)
|
|
||||||
click.echo("Response code: {}".format(response.status_code), err=True)
|
|
||||||
click.echo("Response headers: {}".format(response.headers), err=True)
|
|
||||||
click.echo(err=True)
|
|
||||||
|
|
||||||
# Return the response object
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def get_wait_retdata(response, wait_flag):
|
def get_wait_retdata(response, wait_flag):
|
||||||
|
38
client-cli/pvc/lib/lazy_imports.py
Normal file
38
client-cli/pvc/lib/lazy_imports.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# lazy_imports.py - Lazy module importer library
|
||||||
|
# Part of the Parallel Virtual Cluster (PVC) system
|
||||||
|
#
|
||||||
|
# Copyright (C) 2018-2024 Joshua M. Boniface <joshua@boniface.me>
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, version 3.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
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
|
@@ -19,11 +19,6 @@
|
|||||||
#
|
#
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
from requests_toolbelt.multipart.encoder import (
|
|
||||||
MultipartEncoder,
|
|
||||||
MultipartEncoderMonitor,
|
|
||||||
)
|
|
||||||
|
|
||||||
import pvc.lib.ansiprint as ansiprint
|
import pvc.lib.ansiprint as ansiprint
|
||||||
from pvc.lib.common import UploadProgressBar, call_api, get_wait_retdata
|
from pvc.lib.common import UploadProgressBar, call_api, get_wait_retdata
|
||||||
|
|
||||||
@@ -549,6 +544,12 @@ def ova_upload(config, name, ova_file, params):
|
|||||||
bar = UploadProgressBar(
|
bar = UploadProgressBar(
|
||||||
ova_file, end_message="Parsing file on remote side...", end_nl=False
|
ova_file, end_message="Parsing file on remote side...", end_nl=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from requests_toolbelt.multipart.encoder import (
|
||||||
|
MultipartEncoder,
|
||||||
|
MultipartEncoderMonitor,
|
||||||
|
)
|
||||||
|
|
||||||
upload_data = MultipartEncoder(
|
upload_data = MultipartEncoder(
|
||||||
fields={"file": ("filename", open(ova_file, "rb"), "application/octet-stream")}
|
fields={"file": ("filename", open(ova_file, "rb"), "application/octet-stream")}
|
||||||
)
|
)
|
||||||
|
@@ -23,10 +23,6 @@ import math
|
|||||||
|
|
||||||
from os import path
|
from os import path
|
||||||
from json import loads
|
from json import loads
|
||||||
from requests_toolbelt.multipart.encoder import (
|
|
||||||
MultipartEncoder,
|
|
||||||
MultipartEncoderMonitor,
|
|
||||||
)
|
|
||||||
|
|
||||||
import pvc.lib.ansiprint as ansiprint
|
import pvc.lib.ansiprint as ansiprint
|
||||||
from pvc.lib.common import UploadProgressBar, call_api, get_wait_retdata
|
from pvc.lib.common import UploadProgressBar, call_api, get_wait_retdata
|
||||||
@@ -1212,6 +1208,12 @@ def ceph_volume_upload(config, pool, volume, image_format, image_file):
|
|||||||
bar = UploadProgressBar(
|
bar = UploadProgressBar(
|
||||||
image_file, end_message="Parsing file on remote side...", end_nl=False
|
image_file, end_message="Parsing file on remote side...", end_nl=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from requests_toolbelt.multipart.encoder import (
|
||||||
|
MultipartEncoder,
|
||||||
|
MultipartEncoderMonitor,
|
||||||
|
)
|
||||||
|
|
||||||
upload_data = MultipartEncoder(
|
upload_data = MultipartEncoder(
|
||||||
fields={
|
fields={
|
||||||
"file": ("filename", open(image_file, "rb"), "application/octet-stream")
|
"file": ("filename", open(image_file, "rb"), "application/octet-stream")
|
||||||
|
21
client-cli/pyproject.toml
Normal file
21
client-cli/pyproject.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "pvc"
|
||||||
|
version = "0.9.107"
|
||||||
|
dependencies = [
|
||||||
|
"Click",
|
||||||
|
"PyYAML",
|
||||||
|
"lxml",
|
||||||
|
"colorama",
|
||||||
|
"requests",
|
||||||
|
"requests-toolbelt",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
packages = ["pvc.cli", "pvc.lib"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
pvc = "pvc.cli.cli:cli"
|
@@ -1,20 +0,0 @@
|
|||||||
from setuptools import setup
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="pvc",
|
|
||||||
version="0.9.107",
|
|
||||||
packages=["pvc.cli", "pvc.lib"],
|
|
||||||
install_requires=[
|
|
||||||
"Click",
|
|
||||||
"PyYAML",
|
|
||||||
"lxml",
|
|
||||||
"colorama",
|
|
||||||
"requests",
|
|
||||||
"requests-toolbelt",
|
|
||||||
],
|
|
||||||
entry_points={
|
|
||||||
"console_scripts": [
|
|
||||||
"pvc = pvc.cli.cli:cli",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
2
debian/compat
vendored
2
debian/compat
vendored
@@ -1 +1 @@
|
|||||||
9
|
13
|
||||||
|
3
debian/control
vendored
3
debian/control
vendored
@@ -5,6 +5,7 @@ Maintainer: Joshua Boniface <joshua@boniface.me>
|
|||||||
Standards-Version: 3.9.8
|
Standards-Version: 3.9.8
|
||||||
Homepage: https://www.boniface.me
|
Homepage: https://www.boniface.me
|
||||||
X-Python3-Version: >= 3.7
|
X-Python3-Version: >= 3.7
|
||||||
|
Build-Depends: pybuild-plugin-pyproject, dh-python
|
||||||
|
|
||||||
Package: pvc-daemon-node
|
Package: pvc-daemon-node
|
||||||
Architecture: all
|
Architecture: all
|
||||||
@@ -48,7 +49,7 @@ Description: Parallel Virtual Cluster common libraries
|
|||||||
|
|
||||||
Package: pvc-client-cli
|
Package: pvc-client-cli
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Depends: python3-requests, python3-requests-toolbelt, python3-yaml, python3-lxml, python3-click
|
Depends: python3-requests-toolbelt, python3-yaml, python3-lxml, python3-click
|
||||||
Description: Parallel Virtual Cluster CLI client
|
Description: Parallel Virtual Cluster CLI client
|
||||||
A KVM/Zookeeper/Ceph-based VM and private cloud manager
|
A KVM/Zookeeper/Ceph-based VM and private cloud manager
|
||||||
.
|
.
|
||||||
|
5
debian/rules
vendored
5
debian/rules
vendored
@@ -7,13 +7,14 @@ export DH_VERBOSE = 1
|
|||||||
dh $@ --with python3
|
dh $@ --with python3
|
||||||
|
|
||||||
override_dh_python3:
|
override_dh_python3:
|
||||||
cd $(CURDIR)/client-cli; pybuild --system=distutils --dest-dir=../debian/pvc-client-cli/
|
cd $(CURDIR)/client-cli; pybuild --system=pyproject --dest-dir=../debian/pvc-client-cli/
|
||||||
mkdir -p debian/pvc-client-cli/usr/lib/python3
|
mkdir -p debian/pvc-client-cli/usr/lib/python3
|
||||||
mv debian/pvc-client-cli/usr/lib/python3*/* debian/pvc-client-cli/usr/lib/python3/
|
mv debian/pvc-client-cli/usr/lib/python3*/* debian/pvc-client-cli/usr/lib/python3/
|
||||||
rm -r $(CURDIR)/client-cli/.pybuild $(CURDIR)/client-cli/pvc.egg-info
|
rm -r $(CURDIR)/client-cli/.pybuild $(CURDIR)/client-cli/pvc.egg-info
|
||||||
|
|
||||||
override_dh_auto_clean:
|
override_dh_auto_clean:
|
||||||
find . -name "__pycache__" -o -name ".pybuild" -exec rm -fr {} + || true
|
find $(CURDIR) -name "__pycache__" -o -name ".pybuild" -exec rm -fr {} + || true
|
||||||
|
rm -r $(CURDIR)/client-cli/build || true
|
||||||
|
|
||||||
# If you need to rebuild the Sphinx documentation
|
# If you need to rebuild the Sphinx documentation
|
||||||
# Add spinxdoc to the dh --with line
|
# Add spinxdoc to the dh --with line
|
||||||
|
Reference in New Issue
Block a user