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}\"," 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/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
|
||||
|
||||
changelog_tmpdir=$( mktemp -d )
|
||||
|
@ -1,14 +1,5 @@
|
||||
#!/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
|
||||
#
|
||||
@ -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 difflib import unified_diff
|
||||
from functools import wraps
|
||||
@ -106,10 +108,7 @@ def version(ctx, param, value):
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
|
||||
from pkg_resources import get_distribution
|
||||
|
||||
version = get_distribution("pvc").version
|
||||
echo(CLI_CONFIG, f"Parallel Virtual Cluster CLI client version {version}")
|
||||
echo(CLI_CONFIG, f"Parallel Virtual Cluster CLI client version {VERSION}")
|
||||
ctx.exit()
|
||||
|
||||
|
||||
|
@ -35,15 +35,25 @@ class Response:
|
||||
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:
|
||||
try:
|
||||
# Don't catch JSONDecodeError - let it propagate like requests does
|
||||
self._json = json.loads(self.content.decode('utf-8'))
|
||||
except json.JSONDecodeError:
|
||||
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
|
||||
class ConnectionError(Exception):
|
||||
pass
|
||||
@ -69,6 +79,11 @@ def _encode_params(params):
|
||||
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}")
|
||||
@ -87,8 +102,16 @@ def call_api(
|
||||
"""
|
||||
Make an API call to the PVC API using native Python libraries.
|
||||
"""
|
||||
# Set the connect timeout to 2 seconds
|
||||
timeout = 2.05
|
||||
# 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(
|
||||
@ -163,9 +186,7 @@ def call_api(
|
||||
|
||||
body += f'--{boundary}--\r\n'.encode()
|
||||
|
||||
# Determine the request type and hit the API
|
||||
response = None
|
||||
|
||||
# Use http.client instead of raw sockets for better HTTP protocol handling
|
||||
try:
|
||||
# Special handling for GET with retries
|
||||
if operation == "get":
|
||||
@ -173,175 +194,103 @@ def call_api(
|
||||
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
|
||||
# Create the appropriate connection with separate timeouts
|
||||
if scheme == 'https':
|
||||
context = ssl.create_default_context()
|
||||
import ssl
|
||||
context = None
|
||||
if not config["verify_ssl"]:
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
s = context.wrap_socket(s, server_hostname=host)
|
||||
context = ssl._create_unverified_context()
|
||||
|
||||
# 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
|
||||
import http.client
|
||||
conn = http.client.HTTPSConnection(
|
||||
host,
|
||||
port=port,
|
||||
timeout=connect_timeout, # This is the connection timeout
|
||||
context=context
|
||||
)
|
||||
else:
|
||||
raise
|
||||
import http.client
|
||||
conn = http.client.HTTPConnection(
|
||||
host,
|
||||
port=port,
|
||||
timeout=connect_timeout # This is the connection timeout
|
||||
)
|
||||
|
||||
# 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:]
|
||||
# Make the request
|
||||
conn.request(operation.upper(), full_path, body=body, headers=headers)
|
||||
|
||||
# Parse status code
|
||||
status_line = headers_raw.split('\r\n')[0]
|
||||
status_code = int(status_line.split(' ')[1])
|
||||
# Set a longer timeout for reading the response
|
||||
conn.sock.settimeout(read_timeout)
|
||||
|
||||
# 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()
|
||||
http_response = conn.getresponse()
|
||||
|
||||
# Create response object
|
||||
response = Response(status_code, headers_dict, body_raw)
|
||||
# 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
|
||||
s.close()
|
||||
continue
|
||||
break
|
||||
else:
|
||||
failed = True
|
||||
s.close()
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
failed = True
|
||||
if 's' in locals():
|
||||
s.close()
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
continue
|
||||
finally:
|
||||
if 's' in locals():
|
||||
s.close()
|
||||
|
||||
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})")
|
||||
else:
|
||||
# Create socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(timeout)
|
||||
|
||||
try:
|
||||
# Handle SSL for HTTPS
|
||||
# Handle other HTTP methods
|
||||
if scheme == 'https':
|
||||
context = ssl.create_default_context()
|
||||
import ssl
|
||||
context = None
|
||||
if not config["verify_ssl"]:
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
s = context.wrap_socket(s, server_hostname=host)
|
||||
context = ssl._create_unverified_context()
|
||||
|
||||
# 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
|
||||
import http.client
|
||||
conn = http.client.HTTPSConnection(
|
||||
host,
|
||||
port=port,
|
||||
timeout=connect_timeout, # This is the connection timeout
|
||||
context=context
|
||||
)
|
||||
else:
|
||||
raise
|
||||
import http.client
|
||||
conn = http.client.HTTPConnection(
|
||||
host,
|
||||
port=port,
|
||||
timeout=connect_timeout # This is the connection timeout
|
||||
)
|
||||
|
||||
# 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:]
|
||||
# Make the request
|
||||
conn.request(operation.upper(), full_path, body=body, headers=headers)
|
||||
|
||||
# Parse status code
|
||||
status_line = headers_raw.split('\r\n')[0]
|
||||
status_code = int(status_line.split(' ')[1])
|
||||
# Set a longer timeout for reading the response
|
||||
conn.sock.settimeout(read_timeout)
|
||||
|
||||
# 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()
|
||||
http_response = conn.getresponse()
|
||||
|
||||
# Create response object
|
||||
response = Response(status_code, headers_dict, body_raw)
|
||||
else:
|
||||
raise Exception("Invalid HTTP response")
|
||||
finally:
|
||||
s.close()
|
||||
# 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)
|
||||
code = getattr(response, 'status_code', 504) if 'response' in locals() else 504
|
||||
response = ErrorResponse({"message": message}, code, None)
|
||||
|
||||
# Display debug output
|
||||
@ -351,7 +300,7 @@ def call_api(
|
||||
echo("Response headers: {}".format(response.headers), err=True)
|
||||
echo(err=True)
|
||||
|
||||
# Return the response object
|
||||
# Always return the response object - no special handling
|
||||
return response
|
||||
|
||||
|
||||
|
@ -1,6 +1,23 @@
|
||||
"""
|
||||
Lazy import mechanism for PVC CLI to reduce startup time
|
||||
"""
|
||||
#!/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:
|
||||
"""
|
||||
|
2
debian/control
vendored
2
debian/control
vendored
@ -49,7 +49,7 @@ Description: Parallel Virtual Cluster common libraries
|
||||
|
||||
Package: pvc-client-cli
|
||||
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
|
||||
A KVM/Zookeeper/Ceph-based VM and private cloud manager
|
||||
.
|
||||
|
Reference in New Issue
Block a user