Compare commits
1 Commits
1c69d2196b
...
client-spe
Author | SHA1 | Date | |
---|---|---|---|
2eb0c3944e |
@ -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 )
|
||||||
|
@ -1,14 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
# cli.py - PVC Click CLI main library
|
||||||
# Part of the Parallel Virtual Cluster (PVC) system
|
# Part of the Parallel Virtual Cluster (PVC) system
|
||||||
#
|
#
|
||||||
@ -28,6 +19,17 @@ from pvc.lib.lazy_imports import yaml, click_advanced
|
|||||||
#
|
#
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
|
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
|
||||||
@ -106,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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,15 +35,25 @@ class Response:
|
|||||||
self.headers = headers
|
self.headers = headers
|
||||||
self.content = content
|
self.content = content
|
||||||
self._json = None
|
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):
|
def json(self):
|
||||||
if self._json is None:
|
if self._json is None:
|
||||||
try:
|
# Don't catch JSONDecodeError - let it propagate like requests does
|
||||||
self._json = json.loads(self.content.decode('utf-8'))
|
self._json = json.loads(self.content.decode('utf-8'))
|
||||||
except json.JSONDecodeError:
|
|
||||||
self._json = {}
|
|
||||||
return self._json
|
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
|
# Define ConnectionError to mimic requests.exceptions.ConnectionError
|
||||||
class ConnectionError(Exception):
|
class ConnectionError(Exception):
|
||||||
pass
|
pass
|
||||||
@ -69,6 +79,11 @@ def _encode_params(params):
|
|||||||
value = str(value).lower()
|
value = str(value).lower()
|
||||||
elif value is None:
|
elif value is None:
|
||||||
value = ''
|
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:
|
else:
|
||||||
value = str(value)
|
value = str(value)
|
||||||
parts.append(f"{key}={value}")
|
parts.append(f"{key}={value}")
|
||||||
@ -87,8 +102,16 @@ def call_api(
|
|||||||
"""
|
"""
|
||||||
Make an API call to the PVC API using native Python libraries.
|
Make an API call to the PVC API using native Python libraries.
|
||||||
"""
|
"""
|
||||||
# Set the connect timeout to 2 seconds
|
# Set timeouts - fast connection timeout, longer read timeout
|
||||||
timeout = 2.05
|
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
|
# Craft the URI
|
||||||
uri = "{}://{}{}{}".format(
|
uri = "{}://{}{}{}".format(
|
||||||
@ -163,9 +186,7 @@ def call_api(
|
|||||||
|
|
||||||
body += f'--{boundary}--\r\n'.encode()
|
body += f'--{boundary}--\r\n'.encode()
|
||||||
|
|
||||||
# Determine the request type and hit the API
|
# Use http.client instead of raw sockets for better HTTP protocol handling
|
||||||
response = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Special handling for GET with retries
|
# Special handling for GET with retries
|
||||||
if operation == "get":
|
if operation == "get":
|
||||||
@ -173,175 +194,103 @@ def call_api(
|
|||||||
for i in range(3):
|
for i in range(3):
|
||||||
failed = False
|
failed = False
|
||||||
try:
|
try:
|
||||||
# Create socket
|
# Create the appropriate connection with separate timeouts
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
s.settimeout(timeout)
|
|
||||||
|
|
||||||
# Handle SSL for HTTPS
|
|
||||||
if scheme == 'https':
|
if scheme == 'https':
|
||||||
context = ssl.create_default_context()
|
import ssl
|
||||||
|
context = None
|
||||||
if not config["verify_ssl"]:
|
if not config["verify_ssl"]:
|
||||||
context.check_hostname = False
|
context = ssl._create_unverified_context()
|
||||||
context.verify_mode = ssl.CERT_NONE
|
|
||||||
s = context.wrap_socket(s, server_hostname=host)
|
|
||||||
|
|
||||||
# Connect
|
import http.client
|
||||||
s.connect((host, port))
|
conn = http.client.HTTPSConnection(
|
||||||
|
host,
|
||||||
# Build request
|
port=port,
|
||||||
request = f"{operation.upper()} {full_path} HTTP/1.1\r\n"
|
timeout=connect_timeout, # This is the connection timeout
|
||||||
request += f"Host: {host}\r\n"
|
context=context
|
||||||
|
)
|
||||||
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:
|
else:
|
||||||
raise
|
import http.client
|
||||||
|
conn = http.client.HTTPConnection(
|
||||||
|
host,
|
||||||
|
port=port,
|
||||||
|
timeout=connect_timeout # This is the connection timeout
|
||||||
|
)
|
||||||
|
|
||||||
# Parse response
|
# Make the request
|
||||||
if b'\r\n\r\n' in response_data:
|
conn.request(operation.upper(), full_path, body=body, headers=headers)
|
||||||
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
|
# Set a longer timeout for reading the response
|
||||||
status_line = headers_raw.split('\r\n')[0]
|
conn.sock.settimeout(read_timeout)
|
||||||
status_code = int(status_line.split(' ')[1])
|
|
||||||
|
|
||||||
# Parse headers
|
http_response = conn.getresponse()
|
||||||
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
|
# Read response data
|
||||||
response = Response(status_code, headers_dict, body_raw)
|
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:
|
if response.status_code in retry_on_code:
|
||||||
failed = True
|
failed = True
|
||||||
s.close()
|
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
else:
|
|
||||||
failed = True
|
|
||||||
s.close()
|
|
||||||
continue
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed = True
|
failed = True
|
||||||
if 's' in locals():
|
if 'conn' in locals():
|
||||||
s.close()
|
conn.close()
|
||||||
continue
|
continue
|
||||||
finally:
|
|
||||||
if 's' in locals():
|
|
||||||
s.close()
|
|
||||||
|
|
||||||
if failed:
|
if failed:
|
||||||
error = f"Code {response.status_code}" if response else "Timeout"
|
error = f"Code {response.status_code}" if 'response' in locals() else "Timeout"
|
||||||
raise ConnectionError(f"Failed to connect after 3 tries ({error})")
|
raise ConnectionError(f"Failed to connect after 3 tries ({error})")
|
||||||
else:
|
else:
|
||||||
# Create socket
|
# Handle other HTTP methods
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
s.settimeout(timeout)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Handle SSL for HTTPS
|
|
||||||
if scheme == 'https':
|
if scheme == 'https':
|
||||||
context = ssl.create_default_context()
|
import ssl
|
||||||
|
context = None
|
||||||
if not config["verify_ssl"]:
|
if not config["verify_ssl"]:
|
||||||
context.check_hostname = False
|
context = ssl._create_unverified_context()
|
||||||
context.verify_mode = ssl.CERT_NONE
|
|
||||||
s = context.wrap_socket(s, server_hostname=host)
|
|
||||||
|
|
||||||
# Connect
|
import http.client
|
||||||
s.connect((host, port))
|
conn = http.client.HTTPSConnection(
|
||||||
|
host,
|
||||||
# Build request
|
port=port,
|
||||||
request = f"{operation.upper()} {full_path} HTTP/1.1\r\n"
|
timeout=connect_timeout, # This is the connection timeout
|
||||||
request += f"Host: {host}\r\n"
|
context=context
|
||||||
|
)
|
||||||
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:
|
else:
|
||||||
raise
|
import http.client
|
||||||
|
conn = http.client.HTTPConnection(
|
||||||
|
host,
|
||||||
|
port=port,
|
||||||
|
timeout=connect_timeout # This is the connection timeout
|
||||||
|
)
|
||||||
|
|
||||||
# Parse response
|
# Make the request
|
||||||
if b'\r\n\r\n' in response_data:
|
conn.request(operation.upper(), full_path, body=body, headers=headers)
|
||||||
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
|
# Set a longer timeout for reading the response
|
||||||
status_line = headers_raw.split('\r\n')[0]
|
conn.sock.settimeout(read_timeout)
|
||||||
status_code = int(status_line.split(' ')[1])
|
|
||||||
|
|
||||||
# Parse headers
|
http_response = conn.getresponse()
|
||||||
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
|
# Read response data
|
||||||
response = Response(status_code, headers_dict, body_raw)
|
status_code = http_response.status
|
||||||
else:
|
response_headers = dict(http_response.getheaders())
|
||||||
raise Exception("Invalid HTTP response")
|
response_data = http_response.read()
|
||||||
finally:
|
|
||||||
s.close()
|
conn.close()
|
||||||
|
|
||||||
|
# Create a Response object
|
||||||
|
response = Response(status_code, response_headers, response_data)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
message = f"Failed to connect to the API: {e}"
|
message = f"Failed to connect to the API: {e}"
|
||||||
code = getattr(response, 'status_code', 504)
|
code = getattr(response, 'status_code', 504) if 'response' in locals() else 504
|
||||||
response = ErrorResponse({"message": message}, code, None)
|
response = ErrorResponse({"message": message}, code, None)
|
||||||
|
|
||||||
# Display debug output
|
# Display debug output
|
||||||
@ -351,7 +300,7 @@ def call_api(
|
|||||||
echo("Response headers: {}".format(response.headers), err=True)
|
echo("Response headers: {}".format(response.headers), err=True)
|
||||||
echo(err=True)
|
echo(err=True)
|
||||||
|
|
||||||
# Return the response object
|
# Always return the response object - no special handling
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,23 @@
|
|||||||
"""
|
#!/usr/bin/env python3
|
||||||
Lazy import mechanism for PVC CLI to reduce startup time
|
|
||||||
"""
|
# 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:
|
class LazyModule:
|
||||||
"""
|
"""
|
||||||
|
2
debian/control
vendored
2
debian/control
vendored
@ -49,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
|
||||||
.
|
.
|
||||||
|
Reference in New Issue
Block a user