Add support for https in USM software client
This change add support for https with SSL protocol and certificate. The USM client can work with either insecure (disable SSL/TLS certificate verification) or with SSL certificate. The client is also modified to support sessions and versions. These changes are adapted from cgtsclient. This adds three authorization modes, [token, keystone & local-root]. In token mode, a keystone token and software-url is used for auth. Eg: $ software \ --software-url "http://192.168.204.1:5497" \ --os-auth-token "${TOKEN}" list In keystone mode, sourced keystone configs in env is used for auth. Eg: $ source /etc/platform/openrc; software list In local-root mode, authorization is by privileged user (root/sudo) of the controller where software application is running. Eg: $ sudo software list Optional arguments specific to https: -k, --insecure --cert-file CERT_FILE --key-file KEY_FILE --ca-file CA_FILE Example usage for insecure connection: software -k list Story: 2010676 Task: 49666 Test Plan: PASS: Verify software cli output for http endpoints PASS: Verify software cli output for https endpoints Change-Id: I2e2ff115b8d03cddb02e026da84f389918238dab Signed-off-by: Joseph Vazhappilly <joseph.vazhappillypaily@windriver.com>
This commit is contained in:
parent
202751d57b
commit
0cd1d59425
@ -1,2 +1,14 @@
|
||||
# The version of this component
|
||||
VERSION = 1.0
|
||||
#
|
||||
# Copyright (c) 2013-2024 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
try:
|
||||
import software_client.client
|
||||
Client = software_client.client.Client
|
||||
except ImportError:
|
||||
import warnings
|
||||
warnings.warn("Could not import software_client.client", ImportWarning)
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
174
software-client/software_client/client.py
Normal file
174
software-client/software_client/client.py
Normal file
@ -0,0 +1,174 @@
|
||||
#
|
||||
# Copyright (c) 2013-2024 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
from keystoneauth1 import loading
|
||||
from oslo_utils import importutils
|
||||
|
||||
from software_client import exc
|
||||
from software_client.constants import TOKEN, KEYSTONE, LOCAL_ROOT
|
||||
|
||||
|
||||
SERVICE_NAME = 'usm'
|
||||
SERVICE_TYPE = 'usm'
|
||||
API_PORT = "5493"
|
||||
API_ENDPOINT = "http://127.0.0.1:" + API_PORT
|
||||
|
||||
|
||||
def _make_session(**kwargs):
|
||||
"""Construct a session based on authentication information
|
||||
|
||||
:param kwargs: keyword args containing credentials, either:
|
||||
* os_auth_token: pre-existing token to re-use
|
||||
* system_url: system API endpoint
|
||||
or:
|
||||
* os_username: name of user
|
||||
* os_password: user's password
|
||||
* os_auth_url: endpoint to authenticate against
|
||||
* insecure: allow insecure SSL (no cert verification)
|
||||
* os_tenant_{name|id}: name or ID of tenant
|
||||
* os_region_name: region of the service
|
||||
* os_project_name: name of a project
|
||||
* os_project_id: ID of a project
|
||||
* os_user_domain_name: name of a domain the user belongs to
|
||||
* os_user_domain_id: ID of a domain the user belongs to
|
||||
* os_project_domain_name: name of a domain the project belongs to
|
||||
* os_project_domain_id: ID of a domain the project belongs to
|
||||
* timeout: request timeout (in seconds)
|
||||
* ca_file: trusted CA file
|
||||
* cert_file: client certificate file
|
||||
* key_file: client key file
|
||||
"""
|
||||
session = None
|
||||
if (kwargs.get('os_username') and
|
||||
kwargs.get('os_password') and
|
||||
kwargs.get('os_auth_url') and
|
||||
(kwargs.get('os_project_id') or
|
||||
kwargs.get('os_project_name'))):
|
||||
auth_kwargs = {}
|
||||
auth_url = kwargs.get('os_auth_url')
|
||||
project_id = kwargs.get('os_project_id')
|
||||
project_name = kwargs.get('os_project_name')
|
||||
user_domain_id = kwargs.get('os_user_domain_id')
|
||||
user_domain_name = kwargs.get('os_user_domain_name') or "Default"
|
||||
project_domain_id = kwargs.get('os_project_domain_id')
|
||||
project_domain_name = kwargs.get('os_project_domain_name') or "Default"
|
||||
|
||||
auth_type = 'password'
|
||||
username = kwargs.get('os_username')
|
||||
password = kwargs.get('os_password')
|
||||
auth_kwargs.update({
|
||||
'auth_url': auth_url,
|
||||
'project_id': project_id,
|
||||
'project_name': project_name,
|
||||
'user_domain_id': user_domain_id,
|
||||
'user_domain_name': user_domain_name,
|
||||
'project_domain_id': project_domain_id,
|
||||
'project_domain_name': project_domain_name,
|
||||
'username': username,
|
||||
'password': password
|
||||
})
|
||||
|
||||
# construct the appropriate session
|
||||
timeout = kwargs.get('timeout')
|
||||
insecure = kwargs.get('insecure')
|
||||
cacert = kwargs.get('ca_file')
|
||||
cert = kwargs.get('cert_file')
|
||||
key = kwargs.get('key_file')
|
||||
|
||||
loader = loading.get_plugin_loader(auth_type)
|
||||
auth_plugin = loader.load_from_options(**auth_kwargs)
|
||||
session = loading.session.Session().load_from_options(auth=auth_plugin,
|
||||
timeout=timeout,
|
||||
insecure=insecure,
|
||||
cacert=cacert,
|
||||
cert=cert,
|
||||
key=key)
|
||||
# session could still be None
|
||||
return session
|
||||
|
||||
|
||||
def get_client(api_version, auth_mode, session=None, service_type=SERVICE_TYPE, **kwargs):
|
||||
"""Get an authenticated client, based on credentials in the keyword args.
|
||||
|
||||
:param api_version: the API version to use ('1' or '2')
|
||||
:param auth_mode: the authentication mode (token, keystone, local_root)
|
||||
:param session: the session to use (if it exists)
|
||||
:param service_type: service_type should always be 'usm'
|
||||
:param kwargs: additional keyword args to pass to the client or auth
|
||||
"""
|
||||
endpoint = kwargs.get('software_url')
|
||||
|
||||
auth_token = kwargs.get('os_auth_token')
|
||||
local_root = auth_mode == LOCAL_ROOT
|
||||
# if we have an endpoint and token, use those
|
||||
if local_root or (endpoint and auth_token):
|
||||
pass
|
||||
elif not session:
|
||||
# Make a session to determine the endpoint
|
||||
session = _make_session(**kwargs)
|
||||
|
||||
if not endpoint:
|
||||
exception_msg = ('Either provide Keystone credentials or '
|
||||
'user-defined endpoint and token or '
|
||||
'execute software command as root (sudo)')
|
||||
if session:
|
||||
try:
|
||||
interface = kwargs.get('os_endpoint_type')
|
||||
region_name = kwargs.get('os_region_name')
|
||||
endpoint = session.get_endpoint(service_type=service_type,
|
||||
interface=interface,
|
||||
region_name=region_name)
|
||||
except Exception as e:
|
||||
raise exc.EndpointException(
|
||||
('%(message)s, error was: %(error)s') %
|
||||
{'message': exception_msg, 'error': e})
|
||||
elif local_root:
|
||||
endpoint = API_ENDPOINT
|
||||
else:
|
||||
raise exc.AmbigiousAuthSystem(exception_msg)
|
||||
|
||||
if endpoint:
|
||||
api_version_str = 'v' + api_version
|
||||
if api_version_str not in endpoint.split('/'):
|
||||
endpoint = endpoint + '/' + api_version_str
|
||||
|
||||
if session:
|
||||
# this will be a LegacyJsonAdapter
|
||||
cli_kwargs = {
|
||||
'session': session,
|
||||
'service_type': service_type,
|
||||
'service_name': SERVICE_NAME,
|
||||
'interface': kwargs.get('os_endpoint_type'),
|
||||
'region_name': kwargs.get('os_region_name'),
|
||||
'endpoint_override': endpoint,
|
||||
'global_request_id': kwargs.get('global_request_id'),
|
||||
'user_agent': kwargs.get('user_agent', 'software_client'),
|
||||
'api_version': kwargs.get('system_api_version')
|
||||
}
|
||||
else:
|
||||
# This will become a httplib2 object
|
||||
auth_ref = None
|
||||
cli_kwargs = {
|
||||
'local_root': local_root,
|
||||
'token': auth_token,
|
||||
'insecure': kwargs.get('insecure'),
|
||||
'cacert': kwargs.get('cacert'),
|
||||
'timeout': kwargs.get('timeout'),
|
||||
'ca_file': kwargs.get('ca_file'),
|
||||
'cert_file': kwargs.get('cert_file'),
|
||||
'key_file': kwargs.get('key_file'),
|
||||
'auth_ref': auth_ref,
|
||||
'auth_url': kwargs.get('os_auth_url'),
|
||||
'api_version': kwargs.get('system_api_version')
|
||||
}
|
||||
return Client(api_version, endpoint, session, **cli_kwargs)
|
||||
|
||||
|
||||
def Client(version, *args, **kwargs):
|
||||
module = importutils.import_versioned_module('software_client',
|
||||
version, 'client')
|
||||
client_class = getattr(module, 'Client')
|
||||
return client_class(*args, **kwargs)
|
0
software-client/software_client/common/__init__.py
Normal file
0
software-client/software_client/common/__init__.py
Normal file
130
software-client/software_client/common/base.py
Normal file
130
software-client/software_client/common/base.py
Normal file
@ -0,0 +1,130 @@
|
||||
# Copyright 2013-2024 Wind River, Inc.
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
|
||||
class Manager(object):
|
||||
"""Managers interact with a particular type of API and provide CRUD
|
||||
operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
def _create(self, url, **kwargs):
|
||||
return self.api.json_request('POST', url, **kwargs)
|
||||
|
||||
def _create_multipart(self, url, **kwargs):
|
||||
return self.api.multipart_request('POST', url, **kwargs)
|
||||
|
||||
def _list(self, url, response_key=None, obj_class=None, body=None):
|
||||
resp, body = self.api.json_request('GET', url)
|
||||
if response_key:
|
||||
try:
|
||||
data = body[response_key]
|
||||
except KeyError:
|
||||
return []
|
||||
else:
|
||||
data = body
|
||||
|
||||
return resp, data
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""A resource represents a particular instance of an object (tenant, user,
|
||||
etc). This is pretty much just a bag for attributes.
|
||||
|
||||
:param manager: Manager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
def __init__(self, manager, info, loaded=False):
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
self._loaded = loaded
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in info.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
def __getattr__(self, k):
|
||||
if k not in self.__dict__:
|
||||
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
||||
if not self.is_loaded():
|
||||
self.get()
|
||||
return self.__getattr__(k)
|
||||
|
||||
raise AttributeError(k)
|
||||
else:
|
||||
return self.__dict__[k]
|
||||
|
||||
# deepcopy is invoked on this object which causes infinite recursion in python3
|
||||
# unless the copy and deepcopy methods are overridden
|
||||
def __copy__(self):
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls)
|
||||
result.__dict__.update(self.__dict__)
|
||||
return result
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls)
|
||||
memo[id(self)] = result
|
||||
for k, v in self.__dict__.items():
|
||||
setattr(result, k, copy.deepcopy(v, memo))
|
||||
return result
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k for k in list(self.__dict__.keys()) if k[0] != '_' and
|
||||
k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
return "<%s %s>" % (self.__class__.__name__, info)
|
||||
|
||||
def get(self):
|
||||
# set_loaded() first ... so if we have to bail, we know we tried.
|
||||
self.set_loaded(True)
|
||||
if not hasattr(self.manager, 'get'):
|
||||
return
|
||||
|
||||
new = self.manager.get(self.id)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||
return self.id == other.id
|
||||
return self._info == other._info
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.manager, self._info, self._loaded))
|
||||
|
||||
def is_loaded(self):
|
||||
return self._loaded
|
||||
|
||||
def set_loaded(self, val):
|
||||
self._loaded = val
|
||||
|
||||
def to_dict(self):
|
||||
return copy.deepcopy(self._info)
|
696
software-client/software_client/common/http.py
Normal file
696
software-client/software_client/common/http.py
Normal file
@ -0,0 +1,696 @@
|
||||
# Copyright 2013-2024 Wind River, Inc.
|
||||
# Copyright 2012 Openstack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import httplib2
|
||||
from keystoneauth1 import adapter
|
||||
import logging
|
||||
import os
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
import requests
|
||||
from requests_toolbelt import MultipartEncoder
|
||||
import socket
|
||||
|
||||
import six
|
||||
from six.moves.urllib.parse import urlparse
|
||||
|
||||
|
||||
try:
|
||||
import ssl
|
||||
except ImportError:
|
||||
# TODO(bcwaldon): Handle this failure more gracefully
|
||||
pass
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from software_client import exc as exceptions
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
CHUNKSIZE = 1024 * 64 # 64kB
|
||||
SENSITIVE_HEADERS = ('X-Auth-Token',)
|
||||
UPLOAD_REQUEST_TIMEOUT = 1800
|
||||
USER_AGENT = 'software_client'
|
||||
API_VERSION = '/v1'
|
||||
DEFAULT_API_VERSION = 'latest'
|
||||
|
||||
# httplib2 retries requests on socket.timeout which
|
||||
# is not idempotent and can lead to orhan objects.
|
||||
# See: https://code.google.com/p/httplib2/issues/detail?id=124
|
||||
httplib2.RETRIES = 1
|
||||
|
||||
if os.environ.get('SOFTWARE_CLIENT_DEBUG'):
|
||||
ch = logging.StreamHandler()
|
||||
_logger.setLevel(logging.DEBUG)
|
||||
_logger.addHandler(ch)
|
||||
|
||||
|
||||
class ServiceCatalog(object):
|
||||
"""Helper methods for dealing with a Keystone Service Catalog."""
|
||||
|
||||
def __init__(self, resource_dict):
|
||||
self.catalog = resource_dict
|
||||
|
||||
def get_token(self):
|
||||
"""Fetch token details fron service catalog."""
|
||||
token = {'id': self.catalog['access']['token']['id'],
|
||||
'expires': self.catalog['access']['token']['expires'], }
|
||||
try:
|
||||
token['user_id'] = self.catalog['access']['user']['id']
|
||||
token['tenant_id'] = (
|
||||
self.catalog['access']['token']['tenant']['id'])
|
||||
except Exception:
|
||||
# just leave the tenant and user out if it doesn't exist
|
||||
pass
|
||||
return token
|
||||
|
||||
def url_for(self, attr=None, filter_value=None,
|
||||
service_type='usm', endpoint_type='publicURL'):
|
||||
"""Fetch the URL from the Neutron service for
|
||||
a particular endpoint type. If none given, return
|
||||
publicURL.
|
||||
"""
|
||||
catalog = self.catalog['access'].get('serviceCatalog', [])
|
||||
matching_endpoints = []
|
||||
for service in catalog:
|
||||
if service['type'] != service_type:
|
||||
continue
|
||||
|
||||
endpoints = service['endpoints']
|
||||
for endpoint in endpoints:
|
||||
if not filter_value or endpoint.get(attr) == filter_value:
|
||||
matching_endpoints.append(endpoint)
|
||||
|
||||
if not matching_endpoints:
|
||||
raise exceptions.EndpointNotFound()
|
||||
elif len(matching_endpoints) > 1:
|
||||
raise exceptions.AmbiguousEndpoints(reason=matching_endpoints)
|
||||
else:
|
||||
if endpoint_type not in matching_endpoints[0]:
|
||||
raise exceptions.EndpointTypeNotFound(reason=endpoint_type)
|
||||
|
||||
return matching_endpoints[0][endpoint_type]
|
||||
|
||||
|
||||
class SessionClient(adapter.LegacyJsonAdapter):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user_agent = USER_AGENT
|
||||
self.api_version = 'v' + kwargs.pop('api_version')
|
||||
super(SessionClient, self).__init__(*args, **kwargs)
|
||||
|
||||
def _http_request(self, url, method, **kwargs):
|
||||
version_str = '/' + self.api_version
|
||||
if url.startswith(version_str):
|
||||
url = url[len(version_str):]
|
||||
|
||||
kwargs.setdefault('user_agent', self.user_agent)
|
||||
kwargs.setdefault('auth', self.auth)
|
||||
kwargs.setdefault('endpoint_override', self.endpoint_override)
|
||||
|
||||
# Copy the kwargs so we can reuse the original in case of redirects
|
||||
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
|
||||
kwargs['headers'].setdefault('User-Agent', self.user_agent)
|
||||
|
||||
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
|
||||
endpoint_filter.setdefault('interface', self.interface)
|
||||
endpoint_filter.setdefault('service_type', self.service_type)
|
||||
endpoint_filter.setdefault('region_name', self.region_name)
|
||||
|
||||
return self.session.request(url, method,
|
||||
raise_exc=False, **kwargs)
|
||||
|
||||
def json_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||
kwargs['headers'].setdefault('Accept', 'application/json')
|
||||
if 'body' in kwargs:
|
||||
kwargs['data'] = jsonutils.dumps(kwargs.pop('body'))
|
||||
|
||||
resp = self._http_request(url, method, **kwargs)
|
||||
body = resp.content
|
||||
content_type = resp.headers.get('content-type', None)
|
||||
status = resp.status_code
|
||||
if status == 204 or status == 205 or content_type is None:
|
||||
return resp, list()
|
||||
if 'application/json' in content_type:
|
||||
try:
|
||||
body = resp.json()
|
||||
except ValueError:
|
||||
_logger.error('Could not decode response body as JSON')
|
||||
else:
|
||||
body = None
|
||||
return resp, body
|
||||
|
||||
def multipart_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||
kwargs['headers'].setdefault('Accept', 'application/json')
|
||||
if 'body' in kwargs:
|
||||
kwargs['data'] = kwargs.pop('body')
|
||||
|
||||
resp = self._http_request(url, method, **kwargs)
|
||||
body = resp.content
|
||||
content_type = resp.headers.get('content-type', None)
|
||||
status = resp.status_code
|
||||
if status == 204 or status == 205 or content_type is None:
|
||||
return resp, list()
|
||||
if 'application/json' in content_type:
|
||||
try:
|
||||
body = resp.json()
|
||||
except ValueError:
|
||||
_logger.error('Could not decode response body as JSON')
|
||||
else:
|
||||
body = None
|
||||
return resp, body
|
||||
|
||||
|
||||
|
||||
def raw_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type',
|
||||
'application/octet-stream')
|
||||
return self._http_request(url, method, **kwargs)
|
||||
|
||||
def _get_connection_url(self, url):
|
||||
endpoint = self.endpoint_override
|
||||
version = self.api_version
|
||||
# if 'v1 in both, remove 'v1' from endpoint
|
||||
if version in endpoint and version in url:
|
||||
endpoint = endpoint.replace('/' + version, '', 1)
|
||||
# if 'v1 not in both, add 'v1' to endpoint
|
||||
elif version not in endpoint and version not in url:
|
||||
endpoint = endpoint.rstrip('/') + '/' + version
|
||||
|
||||
return endpoint.rstrip('/') + '/' + url.lstrip('/')
|
||||
|
||||
def upload_request_with_data(self, method, url, **kwargs):
|
||||
requests_url = self._get_connection_url(url)
|
||||
headers = {"X-Auth-Token": self.session.get_token()}
|
||||
files = {'file': ("for_upload",
|
||||
kwargs['body'],
|
||||
)}
|
||||
data = kwargs.get('data')
|
||||
req = requests.post(requests_url, headers=headers, files=files,
|
||||
data=data)
|
||||
return req.json()
|
||||
|
||||
def upload_request_with_multipart(self, method, url, **kwargs):
|
||||
requests_url = self._get_connection_url(url)
|
||||
fields = kwargs.get('data')
|
||||
|
||||
enc = MultipartEncoder(fields)
|
||||
headers = {'Content-Type': enc.content_type,
|
||||
"X-Auth-Token": self.session.get_token()}
|
||||
response = requests.post(requests_url, data=enc, headers=headers)
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
class HTTPClient(httplib2.Http):
|
||||
"""Handles the REST calls and responses, include authn."""
|
||||
|
||||
#################
|
||||
# INIT
|
||||
#################
|
||||
def __init__(self, endpoint,
|
||||
username=None, tenant_name=None, tenant_id=None,
|
||||
password=None, auth_url=None,
|
||||
token=None, region_name=None, timeout=7200,
|
||||
endpoint_url=None, insecure=False,
|
||||
endpoint_type='publicURL',
|
||||
ca_cert=None, log_credentials=False,
|
||||
**kwargs):
|
||||
if 'ca_file' in kwargs and kwargs['ca_file']:
|
||||
ca_cert = kwargs['ca_file']
|
||||
|
||||
super(HTTPClient, self).__init__(timeout=timeout, ca_certs=ca_cert)
|
||||
|
||||
self.username = username
|
||||
self.tenant_name = tenant_name
|
||||
self.tenant_id = tenant_id
|
||||
self.password = password
|
||||
self.auth_url = auth_url.rstrip('/') if auth_url else None
|
||||
self.endpoint_type = endpoint_type
|
||||
self.region_name = region_name
|
||||
self.auth_token = token
|
||||
self.auth_tenant_id = None
|
||||
self.auth_user_id = None
|
||||
self.content_type = 'application/json'
|
||||
self.endpoint_url = endpoint
|
||||
self.log_credentials = log_credentials
|
||||
self.connection_params = self.get_connection_params(self.endpoint_url, **kwargs)
|
||||
self.local_root = kwargs.get('local_root', False)
|
||||
self.api_version = 'v' + kwargs.pop('api_version')
|
||||
|
||||
# httplib2 overrides
|
||||
self.disable_ssl_certificate_validation = insecure
|
||||
|
||||
self.service_catalog = None
|
||||
|
||||
#################
|
||||
# REQUEST
|
||||
#################
|
||||
|
||||
@staticmethod
|
||||
def http_log_resp(_logger, resp, body=None):
|
||||
if not _logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
|
||||
resp_status_code = resp.get('status_code') or ""
|
||||
resp_headers = resp.get('headers') or ""
|
||||
_logger.debug("RESP:%(code)s %(headers)s %(body)s\n",
|
||||
{'code': resp_status_code,
|
||||
'headers': resp_headers,
|
||||
'body': body})
|
||||
|
||||
@staticmethod
|
||||
def http_log_req(_logger, args, kwargs):
|
||||
if not _logger.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
|
||||
string_parts = ['curl -i']
|
||||
for element in args:
|
||||
if element in ('GET', 'POST', 'DELETE', 'PUT'):
|
||||
string_parts.append(' -X %s' % element)
|
||||
else:
|
||||
string_parts.append(' %s' % element)
|
||||
|
||||
for (key, value) in kwargs['headers'].items():
|
||||
if key in SENSITIVE_HEADERS:
|
||||
v = value.encode('utf-8')
|
||||
h = hashlib.sha256(v)
|
||||
d = h.hexdigest()
|
||||
value = "{SHA256}%s" % d
|
||||
header = ' -H "%s: %s"' % (key, value)
|
||||
string_parts.append(header)
|
||||
|
||||
if 'body' in kwargs and kwargs['body']:
|
||||
string_parts.append(" -d '%s'" % (kwargs['body']))
|
||||
req = encodeutils.safe_encode("".join(string_parts))
|
||||
_logger.debug("REQ: %s", req)
|
||||
|
||||
def _cs_request(self, *args, **kwargs):
|
||||
kargs = {}
|
||||
kargs.setdefault('headers', kwargs.get('headers', {}))
|
||||
|
||||
if 'content_type' in kwargs:
|
||||
kargs['headers']['Content-Type'] = kwargs['content_type']
|
||||
kargs['headers']['Accept'] = kwargs['content_type']
|
||||
else:
|
||||
kargs['headers']['Content-Type'] = self.content_type
|
||||
kargs['headers']['Accept'] = self.content_type
|
||||
|
||||
if self.auth_token:
|
||||
kargs['headers']['X-Auth-Token'] = self.auth_token
|
||||
|
||||
if 'body' in kwargs:
|
||||
kargs['body'] = kwargs['body']
|
||||
if self.log_credentials:
|
||||
log_kargs = kargs
|
||||
else:
|
||||
log_kargs = self._strip_credentials(kargs)
|
||||
|
||||
self.http_log_req(_logger, args, log_kargs)
|
||||
try:
|
||||
resp, body = self.request(*args, **kargs)
|
||||
except requests.exceptions.SSLError as e:
|
||||
raise exceptions.SslCertificateValidationError(reason=str(e))
|
||||
except Exception as e:
|
||||
# Wrap the low-level connection error (socket timeout, redirect
|
||||
# limit, decompression error, etc) into our custom high-level
|
||||
# connection exception (it is excepted in the upper layers of code)
|
||||
_logger.debug("throwing ConnectionFailed : %s", e)
|
||||
raise exceptions.CommunicationError(str(e))
|
||||
finally:
|
||||
# Temporary Fix for gate failures. RPC calls and HTTP requests
|
||||
# seem to be stepping on each other resulting in bogus fd's being
|
||||
# picked up for making http requests
|
||||
self.connections.clear()
|
||||
|
||||
# Read body into string if it isn't obviously image data
|
||||
body_str = None
|
||||
if 'content-type' in resp and resp['content-type'] != 'application/octet-stream':
|
||||
body_str = ''.join([chunk for chunk in body.decode('utf8')])
|
||||
self.http_log_resp(_logger, resp, body_str)
|
||||
body = body_str
|
||||
else:
|
||||
self.http_log_resp(_logger, resp, body)
|
||||
|
||||
return resp, body
|
||||
|
||||
def json_request(self, method, url, **kwargs):
|
||||
if not self.local_root:
|
||||
self.authenticate_and_fetch_endpoint_url()
|
||||
# Perform the request once. If we get a 401 back then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||
kwargs['headers'].setdefault('Accept', 'application/json')
|
||||
|
||||
if 'body' in kwargs:
|
||||
kwargs['body'] = json.dumps(kwargs['body'])
|
||||
|
||||
connection_url = self._get_connection_url(url)
|
||||
try:
|
||||
resp, body_iter = self._cs_request(connection_url,
|
||||
method, **kwargs)
|
||||
except exceptions.HTTPUnauthorized:
|
||||
self.authenticate()
|
||||
resp, body_iter = self._cs_request(
|
||||
connection_url, method, **kwargs)
|
||||
|
||||
content_type = resp['content-type'] \
|
||||
if resp.get('content-type', None) else None
|
||||
|
||||
if resp.status == 204 or resp.status == 205 or content_type is None:
|
||||
return resp, list()
|
||||
|
||||
if 'application/json' in content_type:
|
||||
body = ''.join([chunk for chunk in body_iter])
|
||||
try:
|
||||
body = json.loads(body)
|
||||
except ValueError:
|
||||
_logger.error('Could not decode response body as JSON')
|
||||
else:
|
||||
body = None
|
||||
|
||||
# Add status_code attribute to make compatible with session resp
|
||||
setattr(resp, 'status_code', resp.status)
|
||||
return resp, body
|
||||
|
||||
def multipart_request(self, method, url, **kwargs):
|
||||
return self.upload_request_with_multipart(method, url, **kwargs)
|
||||
|
||||
|
||||
def raw_request(self, method, url, **kwargs):
|
||||
if not self.local_root:
|
||||
self.authenticate_and_fetch_endpoint_url()
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type',
|
||||
'application/octet-stream')
|
||||
connection_url = self._get_connection_url(url)
|
||||
return self._cs_request(connection_url, method, **kwargs)
|
||||
|
||||
def upload_request_with_data(self, method, url, **kwargs):
|
||||
if not self.local_root:
|
||||
self.authenticate_and_fetch_endpoint_url()
|
||||
connection_url = self._get_connection_url(url)
|
||||
headers = {"X-Auth-Token": self.auth_token}
|
||||
files = {'file': ("for_upload",
|
||||
kwargs['body'],
|
||||
)}
|
||||
data = kwargs.get('data')
|
||||
req = requests.post(connection_url, headers=headers,
|
||||
files=files, data=data,
|
||||
timeout=UPLOAD_REQUEST_TIMEOUT)
|
||||
return req.json()
|
||||
|
||||
def upload_request_with_multipart(self, method, url, **kwargs):
|
||||
if not self.local_root:
|
||||
self.authenticate_and_fetch_endpoint_url()
|
||||
connection_url = self._get_connection_url(url)
|
||||
|
||||
response = requests.post(connection_url,
|
||||
data=kwargs.get('body'),
|
||||
headers=kwargs.get('headers'),
|
||||
timeout=UPLOAD_REQUEST_TIMEOUT)
|
||||
|
||||
return response, response.json()
|
||||
|
||||
#################
|
||||
# AUTHENTICATE
|
||||
#################
|
||||
|
||||
def authenticate_and_fetch_endpoint_url(self):
|
||||
if not self.auth_token:
|
||||
self.authenticate()
|
||||
if not self.endpoint_url:
|
||||
self._get_endpoint_url()
|
||||
|
||||
def authenticate(self):
|
||||
if self.auth_url is None:
|
||||
raise exceptions.HTTPUnauthorized("No auth_url provided")
|
||||
|
||||
token_url = self.auth_url + "/tokens"
|
||||
|
||||
if self.tenant_id:
|
||||
body = {'auth': {'passwordCredentials':
|
||||
{'username': self.username,
|
||||
'password': self.password, },
|
||||
'tenantId': self.tenant_id, }, }
|
||||
else:
|
||||
body = {'auth': {'passwordCredentials':
|
||||
{'username': self.username,
|
||||
'password': self.password, },
|
||||
'tenantName': self.tenant_name, }, }
|
||||
|
||||
resp, resp_body = self._cs_request(token_url, "POST",
|
||||
body=json.dumps(body),
|
||||
content_type="application/json")
|
||||
status_code = self.get_status_code(resp)
|
||||
if status_code != 200:
|
||||
raise exceptions.HTTPUnauthorized(resp_body)
|
||||
if resp_body:
|
||||
try:
|
||||
resp_body = json.loads(resp_body)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
resp_body = None
|
||||
self._extract_service_catalog(resp_body)
|
||||
|
||||
_logger.debug("Authenticated user %s", self.username)
|
||||
|
||||
def get_auth_info(self):
|
||||
return {'auth_token': self.auth_token,
|
||||
'auth_tenant_id': self.auth_tenant_id,
|
||||
'auth_user_id': self.auth_user_id,
|
||||
'endpoint_url': self.endpoint_url}
|
||||
|
||||
#################
|
||||
# UTILS
|
||||
#################
|
||||
def _strip_credentials(self, kwargs):
|
||||
if kwargs.get('body') and self.password:
|
||||
log_kwargs = kwargs.copy()
|
||||
log_kwargs['body'] = kwargs['body'].replace(self.password,
|
||||
'REDACTED')
|
||||
return log_kwargs
|
||||
else:
|
||||
return kwargs
|
||||
|
||||
def _extract_service_catalog(self, body):
|
||||
"""Set the client's service catalog from the response data."""
|
||||
self.service_catalog = ServiceCatalog(body)
|
||||
try:
|
||||
sc = self.service_catalog.get_token()
|
||||
self.auth_token = sc['id']
|
||||
self.auth_tenant_id = sc.get('tenant_id')
|
||||
self.auth_user_id = sc.get('user_id')
|
||||
except KeyError:
|
||||
raise exceptions.HTTPUnauthorized()
|
||||
if not self.endpoint_url:
|
||||
self.endpoint_url = self.service_catalog.url_for(
|
||||
attr='region', filter_value=self.region_name,
|
||||
endpoint_type=self.endpoint_type)
|
||||
|
||||
def _get_endpoint_url(self):
|
||||
url = self.auth_url + '/tokens/%s/endpoints' % self.auth_token
|
||||
try:
|
||||
resp, body = self._cs_request(url, "GET")
|
||||
except exceptions.HTTPUnauthorized:
|
||||
# rollback to authenticate() to handle case when neutron client
|
||||
# is initialized just before the token is expired
|
||||
self.authenticate()
|
||||
return self.endpoint_url
|
||||
|
||||
body = json.loads(body)
|
||||
for endpoint in body.get('endpoints', []):
|
||||
if (endpoint['type'] == 'usm' and endpoint.get('region') == self.region_name):
|
||||
if self.endpoint_type not in endpoint:
|
||||
raise exceptions.EndpointTypeNotFound(
|
||||
reason=self.endpoint_type)
|
||||
return endpoint[self.endpoint_type]
|
||||
|
||||
raise exceptions.EndpointNotFound()
|
||||
|
||||
def _get_connection_url(self, url):
|
||||
(_class, _args, _kwargs) = self.connection_params
|
||||
base_url = _args[2]
|
||||
# Since some packages send endpoint with 'v1' and some don't,
|
||||
# the postprocessing for both options will be done here
|
||||
# Instead of doing a fix in each of these packages
|
||||
endpoint = self.endpoint_url
|
||||
version = self.api_version
|
||||
# if 'v1 in both, remove 'v1' from endpoint
|
||||
if version in base_url and version in url:
|
||||
endpoint = endpoint.replace('/' + version, '', 1)
|
||||
# if 'v1 not in both, add 'v1' to endpoint
|
||||
elif version not in base_url and version not in url:
|
||||
endpoint = endpoint.rstrip('/') + '/' + version
|
||||
|
||||
return endpoint.rstrip('/') + '/' + url.lstrip('/')
|
||||
|
||||
@staticmethod
|
||||
def get_connection_params(endpoint, **kwargs):
|
||||
parts = urlparse(endpoint)
|
||||
|
||||
_args = (parts.hostname, parts.port, parts.path)
|
||||
_kwargs = {'timeout': (float(kwargs.get('timeout'))
|
||||
if kwargs.get('timeout') else 600)}
|
||||
|
||||
if parts.scheme == 'https':
|
||||
_class = VerifiedHTTPSConnection
|
||||
_kwargs['ca_file'] = kwargs.get('ca_file', None)
|
||||
_kwargs['cert_file'] = kwargs.get('cert_file', None)
|
||||
_kwargs['key_file'] = kwargs.get('key_file', None)
|
||||
_kwargs['insecure'] = kwargs.get('insecure', False)
|
||||
elif parts.scheme == 'http':
|
||||
_class = six.moves.http_client.HTTPConnection
|
||||
else:
|
||||
msg = 'Unsupported scheme: %s' % parts.scheme
|
||||
raise exceptions.EndpointException(reason=msg)
|
||||
|
||||
return (_class, _args, _kwargs)
|
||||
|
||||
def get_status_code(self, response):
|
||||
"""Returns the integer status code from the response.
|
||||
|
||||
Either a Webob.Response (used in testing) or httplib.Response
|
||||
is returned.
|
||||
"""
|
||||
if hasattr(response, 'status_int'):
|
||||
return response.status_int
|
||||
else:
|
||||
return response.status
|
||||
|
||||
|
||||
class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection):
|
||||
"""httplib-compatibile connection using client-side SSL authentication
|
||||
|
||||
:see http://code.activestate.com/recipes/
|
||||
577548-https-httplib-client-connection-with-certificate-v/
|
||||
"""
|
||||
|
||||
def __init__(self, host, port, key_file=None, cert_file=None,
|
||||
ca_file=None, timeout=None, insecure=False):
|
||||
six.moves.http_client.HTTPSConnection.__init__(self, host, port,
|
||||
key_file=key_file,
|
||||
cert_file=cert_file)
|
||||
self.key_file = key_file
|
||||
self.cert_file = cert_file
|
||||
if ca_file is not None:
|
||||
self.ca_file = ca_file
|
||||
else:
|
||||
self.ca_file = self.get_system_ca_file()
|
||||
self.timeout = timeout
|
||||
self.insecure = insecure
|
||||
|
||||
def connect(self):
|
||||
"""Connect to a host on a given (SSL) port.
|
||||
If ca_file is pointing somewhere, use it to check Server Certificate.
|
||||
|
||||
Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
|
||||
This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to
|
||||
ssl.wrap_socket(), which forces SSL to check server certificate against
|
||||
our client certificate.
|
||||
"""
|
||||
sock = socket.create_connection((self.host, self.port), self.timeout)
|
||||
|
||||
if self._tunnel_host:
|
||||
self.sock = sock
|
||||
self._tunnel()
|
||||
|
||||
if self.insecure is True:
|
||||
kwargs = {'cert_reqs': ssl.CERT_NONE}
|
||||
else:
|
||||
kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file}
|
||||
|
||||
if self.cert_file:
|
||||
kwargs['certfile'] = self.cert_file
|
||||
if self.key_file:
|
||||
kwargs['keyfile'] = self.key_file
|
||||
|
||||
self.sock = ssl.wrap_socket(sock, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_system_ca_file():
|
||||
"""Return path to system default CA file."""
|
||||
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
|
||||
# Suse, FreeBSD/OpenBSD
|
||||
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
|
||||
'/etc/pki/tls/certs/ca-bundle.crt',
|
||||
'/etc/ssl/ca-bundle.pem',
|
||||
'/etc/ssl/cert.pem']
|
||||
for ca in ca_path:
|
||||
if os.path.exists(ca):
|
||||
return ca
|
||||
return None
|
||||
|
||||
|
||||
class ResponseBodyIterator(object):
|
||||
"""A class that acts as an iterator over an HTTP response."""
|
||||
|
||||
def __init__(self, resp):
|
||||
self.resp = resp
|
||||
|
||||
def __iter__(self):
|
||||
while True:
|
||||
yield six.next() # pylint: disable=next-method-called
|
||||
|
||||
def next(self): # pylint: disable=next-method-defined
|
||||
chunk = self.resp.read(CHUNKSIZE)
|
||||
if chunk:
|
||||
return chunk
|
||||
else:
|
||||
raise StopIteration()
|
||||
|
||||
|
||||
def construct_http_client(endpoint=None, username=None, password=None,
|
||||
endpoint_type=None, auth_url=None, **kwargs):
|
||||
|
||||
session = kwargs.pop('session', None)
|
||||
auth = kwargs.pop('auth', None)
|
||||
|
||||
if session:
|
||||
# SessionClient
|
||||
if 'endpoint_override' not in kwargs and endpoint:
|
||||
kwargs['endpoint_override'] = endpoint
|
||||
|
||||
if 'service_type' not in kwargs:
|
||||
kwargs['service_type'] = 'usm'
|
||||
|
||||
if 'interface' not in kwargs and endpoint_type:
|
||||
kwargs['interface'] = endpoint_type
|
||||
|
||||
if 'region_name' in kwargs:
|
||||
kwargs['additional_headers'] = {
|
||||
'X-Region-Name': kwargs['region_name']}
|
||||
|
||||
return SessionClient(session, auth=auth, **kwargs)
|
||||
else:
|
||||
# httplib2
|
||||
return HTTPClient(endpoint=endpoint, username=username,
|
||||
password=password, endpoint_type=endpoint_type,
|
||||
auth_url=auth_url, **kwargs)
|
431
software-client/software_client/common/utils.py
Normal file
431
software-client/software_client/common/utils.py
Normal file
@ -0,0 +1,431 @@
|
||||
# Copyright 2013-2024 Wind River, Inc
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
from oslo_utils import importutils
|
||||
from six.moves import zip
|
||||
|
||||
|
||||
TERM_WIDTH = 72
|
||||
|
||||
class HelpFormatter(argparse.HelpFormatter):
|
||||
def start_section(self, heading):
|
||||
# Title-case the headings
|
||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||
super(HelpFormatter, self).start_section(heading)
|
||||
|
||||
|
||||
def define_command(subparsers, command, callback, cmd_mapper, unrestricted_cmds):
|
||||
'''Define a command in the subparsers collection.
|
||||
|
||||
:param subparsers: subparsers collection where the command will go
|
||||
:param command: command name
|
||||
:param callback: function that will be used to process the command
|
||||
'''
|
||||
desc = callback.__doc__ or ''
|
||||
help = desc.strip().split('\n')[0]
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(command, help=help,
|
||||
description=desc,
|
||||
add_help=False,
|
||||
formatter_class=HelpFormatter)
|
||||
subparser.add_argument('-h', '--help', action='help',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
func = callback
|
||||
cmd_mapper[command] = subparser
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=func)
|
||||
|
||||
if command in unrestricted_cmds:
|
||||
subparser.set_defaults(restricted=False)
|
||||
|
||||
|
||||
def define_commands_from_module(subparsers, command_module, cmd_mapper, unrestricted_cmds=[]):
|
||||
'''Find all methods beginning with 'do_' in a module, and add them
|
||||
as commands into a subparsers collection.
|
||||
'''
|
||||
for method_name in (a for a in dir(command_module) if a.startswith('do_')):
|
||||
# Commands should be hypen-separated instead of underscores.
|
||||
command = method_name[3:].replace('_', '-')
|
||||
callback = getattr(command_module, method_name)
|
||||
define_command(subparsers, command, callback, cmd_mapper, unrestricted_cmds)
|
||||
|
||||
|
||||
# Decorator for cli-args
|
||||
def arg(*args, **kwargs):
|
||||
def _decorator(func):
|
||||
# Because of the sematics of decorator composition if we just append
|
||||
# to the options list positional options will appear to be backwards.
|
||||
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
|
||||
return func
|
||||
|
||||
return _decorator
|
||||
|
||||
|
||||
def env(*vars, **kwargs):
|
||||
"""Search for the first defined of possibly many env vars
|
||||
|
||||
Returns the first environment variable defined in vars, or
|
||||
returns the default defined in kwargs.
|
||||
"""
|
||||
for v in vars:
|
||||
value = os.environ.get(v, None)
|
||||
if value:
|
||||
return value
|
||||
return kwargs.get('default', '')
|
||||
|
||||
|
||||
def import_versioned_module(version, submodule=None):
|
||||
module = 'software_client.v%s' % version
|
||||
if submodule:
|
||||
module = '.'.join((module, submodule))
|
||||
return importutils.import_module(module)
|
||||
|
||||
|
||||
def check_rc(req, data):
|
||||
rc = 0
|
||||
if req.status_code == 200 and data:
|
||||
if 'error' in data and data["error"] != "":
|
||||
rc = 1
|
||||
else:
|
||||
rc = 1
|
||||
|
||||
return rc
|
||||
|
||||
|
||||
def print_result_list(header_data_list, data_list, has_error, sort_key=0):
|
||||
"""
|
||||
Print a list of data in a simple table format
|
||||
:param header_data_list: Array of header data
|
||||
:param data_list: Array of data
|
||||
:param has_error: Boolean indicating if the request has error message
|
||||
:param sort_key: Sorting key for the list
|
||||
"""
|
||||
|
||||
if has_error:
|
||||
return
|
||||
|
||||
if data_list is None or len(data_list) == 0:
|
||||
return
|
||||
|
||||
# Find the longest header string in each column
|
||||
header_lengths = [len(str(x)) for x in header_data_list]
|
||||
# Find the longest content string in each column
|
||||
content_lengths = [max(len(str(x[i])) for x in data_list)
|
||||
for i in range(len(header_data_list))]
|
||||
# Find the max of the two for each column
|
||||
col_lengths = [(x if x > y else y) for x, y in zip(header_lengths, content_lengths)]
|
||||
|
||||
print(' '.join(f"{x.center(col_lengths[i])}" for i, x in enumerate(header_data_list)))
|
||||
print(' '.join('=' * length for length in col_lengths))
|
||||
for item in sorted(data_list, key=lambda d: d[sort_key]):
|
||||
print(' '.join(f"{str(x).center(col_lengths[i])}" for i, x in enumerate(item)))
|
||||
print("\n")
|
||||
|
||||
|
||||
def print_software_deploy_host_list_result(req, data):
|
||||
if req.status_code == 200 and data:
|
||||
data = data.get("data", None)
|
||||
if not data:
|
||||
print("No deploy in progress.\n")
|
||||
return
|
||||
|
||||
# Calculate column widths
|
||||
hdr_hn = "Hostname"
|
||||
hdr_rel = "Software Release"
|
||||
hdr_tg_rel = "Target Release"
|
||||
hdr_rr = "Reboot Required"
|
||||
hdr_state = "Host State"
|
||||
|
||||
width_hn = len(hdr_hn)
|
||||
width_rel = len(hdr_rel)
|
||||
width_tg_rel = len(hdr_tg_rel)
|
||||
width_rr = len(hdr_rr)
|
||||
width_state = len(hdr_state)
|
||||
|
||||
for agent in sorted(data, key=lambda a: a["hostname"]):
|
||||
if agent.get("host_state") is None:
|
||||
agent["host_state"] = "No active deployment"
|
||||
if agent.get("target_release") is None:
|
||||
agent["target_release"] = "N/A"
|
||||
if len(agent["hostname"]) > width_hn:
|
||||
width_hn = len(agent["hostname"])
|
||||
if len(agent["software_release"]) > width_rel:
|
||||
width_rel = len(agent["software_release"])
|
||||
if len(agent["target_release"]) > width_tg_rel:
|
||||
width_tg_rel = len(agent["target_release"])
|
||||
if len(agent["host_state"]) > width_state:
|
||||
width_state = len(agent["host_state"])
|
||||
|
||||
print("{0:^{width_hn}} {1:^{width_rel}} {2:^{width_tg_rel}} {3:^{width_rr}} {4:^{width_state}}".format(
|
||||
hdr_hn, hdr_rel, hdr_tg_rel, hdr_rr, hdr_state,
|
||||
width_hn=width_hn, width_rel=width_rel, width_tg_rel=width_tg_rel, width_rr=width_rr, width_state=width_state))
|
||||
|
||||
print("{0} {1} {2} {3} {4}".format(
|
||||
'=' * width_hn, '=' * width_rel, '=' * width_tg_rel, '=' * width_rr, '=' * width_state))
|
||||
|
||||
for agent in sorted(data, key=lambda a: a["hostname"]):
|
||||
print("{0:<{width_hn}} {1:^{width_rel}} {2:^{width_tg_rel}} {3:^{width_rr}} {4:^{width_state}}".format(
|
||||
agent["hostname"],
|
||||
agent["software_release"],
|
||||
agent["target_release"],
|
||||
"Yes" if agent.get("reboot_required", None) else "No",
|
||||
agent["host_state"],
|
||||
width_hn=width_hn, width_rel=width_rel, width_tg_rel=width_tg_rel, width_rr=width_rr, width_state=width_state))
|
||||
|
||||
elif req.status_code == 500:
|
||||
print("An internal error has occurred. Please check /var/log/software.log for details")
|
||||
|
||||
|
||||
|
||||
def print_release_show_result(req, data, list_packages=False):
|
||||
if req.status_code == 200:
|
||||
|
||||
if 'metadata' in data:
|
||||
sd = data['metadata']
|
||||
contents = data['contents']
|
||||
for release_id in sorted(list(sd)):
|
||||
print("%s:" % release_id)
|
||||
|
||||
if "sw_version" in sd[release_id] and sd[release_id]["sw_version"] != "":
|
||||
print(textwrap.fill(" {0:<15} ".format("Version:") + sd[release_id]["sw_version"],
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20))
|
||||
|
||||
if "state" in sd[release_id] and sd[release_id]["state"] != "":
|
||||
print(textwrap.fill(" {0:<15} ".format("State:") + sd[release_id]["state"],
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20))
|
||||
|
||||
if "status" in sd[release_id] and sd[release_id]["status"] != "":
|
||||
print(textwrap.fill(" {0:<15} ".format("Status:") + sd[release_id]["status"],
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20))
|
||||
|
||||
if "unremovable" in sd[release_id] and sd[release_id]["unremovable"] != "":
|
||||
print(textwrap.fill(" {0:<15} ".format("Unremovable:") + sd[release_id]["unremovable"],
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20))
|
||||
|
||||
if "reboot_required" in sd[release_id] and sd[release_id]["reboot_required"] != "":
|
||||
print(textwrap.fill(" {0:<15} ".format("RR:") + sd[release_id]["reboot_required"],
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20))
|
||||
|
||||
if "apply_active_release_only" in sd[release_id] and sd[release_id]["apply_active_release_only"] != "":
|
||||
print(textwrap.fill(" {0:<15} ".format("Apply Active Release Only:") + sd[release_id]["apply_active_release_only"],
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20))
|
||||
|
||||
if "summary" in sd[release_id] and sd[release_id]["summary"] != "":
|
||||
print(textwrap.fill(" {0:<15} ".format("Summary:") + sd[release_id]["summary"],
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20))
|
||||
|
||||
if "description" in sd[release_id] and sd[release_id]["description"] != "":
|
||||
first_line = True
|
||||
for line in sd[release_id]["description"].split('\n'):
|
||||
if first_line:
|
||||
print(textwrap.fill(" {0:<15} ".format("Description:") + line,
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20))
|
||||
first_line = False
|
||||
else:
|
||||
print(textwrap.fill(line,
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20,
|
||||
initial_indent=' ' * 20))
|
||||
|
||||
if "install_instructions" in sd[release_id] and sd[release_id]["install_instructions"] != "":
|
||||
print(" Install Instructions:")
|
||||
for line in sd[release_id]["install_instructions"].split('\n'):
|
||||
print(textwrap.fill(line,
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20,
|
||||
initial_indent=' ' * 20))
|
||||
|
||||
if "warnings" in sd[release_id] and sd[release_id]["warnings"] != "":
|
||||
first_line = True
|
||||
for line in sd[release_id]["warnings"].split('\n'):
|
||||
if first_line:
|
||||
print(textwrap.fill(" {0:<15} ".format("Warnings:") + line,
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20))
|
||||
first_line = False
|
||||
else:
|
||||
print(textwrap.fill(line,
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20,
|
||||
initial_indent=' ' * 20))
|
||||
|
||||
if "requires" in sd[release_id] and len(sd[release_id]["requires"]) > 0:
|
||||
print(" Requires:")
|
||||
for req_patch in sorted(sd[release_id]["requires"]):
|
||||
print(' ' * 20 + req_patch)
|
||||
|
||||
if "contents" in data and release_id in data["contents"]:
|
||||
print(" Contents:\n")
|
||||
if "number_of_commits" in contents[release_id] and \
|
||||
contents[release_id]["number_of_commits"] != "":
|
||||
print(textwrap.fill(" {0:<15} ".format("No. of commits:") +
|
||||
contents[release_id]["number_of_commits"],
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20))
|
||||
if "base" in contents[release_id] and \
|
||||
contents[release_id]["base"]["commit"] != "":
|
||||
print(textwrap.fill(" {0:<15} ".format("Base commit:") +
|
||||
contents[release_id]["base"]["commit"],
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20))
|
||||
if "number_of_commits" in contents[release_id] and \
|
||||
contents[release_id]["number_of_commits"] != "":
|
||||
for i in range(int(contents[release_id]["number_of_commits"])):
|
||||
print(textwrap.fill(" {0:<15} ".format("Commit%s:" % (i + 1)) +
|
||||
contents[release_id]["commit%s" % (i + 1)]["commit"],
|
||||
width=TERM_WIDTH, subsequent_indent=' ' * 20))
|
||||
|
||||
if list_packages:
|
||||
if "packages" in sd[release_id] and len(sd[release_id]["packages"]):
|
||||
print(" Packages:")
|
||||
for package in sorted(sd[release_id]["packages"]):
|
||||
print(" " * 20 + package)
|
||||
|
||||
print("\n")
|
||||
|
||||
if 'info' in data and data["info"] != "":
|
||||
print(data["info"])
|
||||
|
||||
if 'warning' in data and data["warning"] != "":
|
||||
print("Warning:")
|
||||
print(data["warning"])
|
||||
|
||||
if 'error' in data and data["error"] != "":
|
||||
print("Error:")
|
||||
print(data["error"])
|
||||
|
||||
elif req.status_code == 500:
|
||||
print("An internal error has occurred. Please check /var/log/software.log for details")
|
||||
|
||||
|
||||
def print_software_op_result(resp, data):
|
||||
if resp.status_code == 200:
|
||||
if 'sd' in data:
|
||||
sd = data['sd']
|
||||
|
||||
# Calculate column widths
|
||||
hdr_release = "Release"
|
||||
hdr_version = "Version"
|
||||
hdr_rr = "RR"
|
||||
hdr_state = "State"
|
||||
|
||||
width_release = len(hdr_release)
|
||||
width_version = len(hdr_version)
|
||||
width_rr = len(hdr_rr)
|
||||
width_state = len(hdr_state)
|
||||
|
||||
show_all = False
|
||||
|
||||
for release_id in list(sd):
|
||||
width_release = max(len(release_id), width_release)
|
||||
width_state = max(len(sd[release_id]["state"]), width_state)
|
||||
if "sw_version" in sd[release_id]:
|
||||
show_all = True
|
||||
width_version = max(len(sd[release_id]["sw_version"]), width_version)
|
||||
|
||||
if show_all:
|
||||
print("{0:^{width_release}} {1:^{width_rr}} {2:^{width_version}} {3:^{width_state}}".format(
|
||||
hdr_release, hdr_rr, hdr_version, hdr_state,
|
||||
width_release=width_release, width_rr=width_rr,
|
||||
width_version=width_version, width_state=width_state))
|
||||
|
||||
print("{0} {1} {2} {3}".format(
|
||||
'=' * width_release, '=' * width_rr, '=' * width_version, '=' * width_state))
|
||||
|
||||
for release_id in sorted(list(sd)):
|
||||
if "reboot_required" in sd[release_id]:
|
||||
rr = sd[release_id]["reboot_required"]
|
||||
else:
|
||||
rr = "Y"
|
||||
|
||||
print("{0:<{width_release}} {1:^{width_rr}} {2:^{width_version}} {3:^{width_state}}".format(
|
||||
release_id,
|
||||
rr,
|
||||
sd[release_id]["sw_version"],
|
||||
sd[release_id]["state"],
|
||||
width_release=width_release, width_rr=width_rr,
|
||||
width_version=width_version, width_state=width_state))
|
||||
else:
|
||||
print("{0:^{width_release}} {1:^{width_state}}".format(
|
||||
hdr_release, hdr_state,
|
||||
width_release=width_release, width_state=width_state))
|
||||
|
||||
print("{0} {1}".format(
|
||||
'=' * width_release, '=' * width_state))
|
||||
|
||||
for release_id in sorted(list(sd)):
|
||||
if "reboot_required" in sd[release_id]:
|
||||
rr = sd[release_id]["reboot_required"]
|
||||
else:
|
||||
rr = "Y"
|
||||
|
||||
print("{0:<{width_release}} {1:^{width_rr}} {2:^{width_state}}".format(
|
||||
release_id,
|
||||
rr,
|
||||
sd[release_id]["state"],
|
||||
width_release=width_release, width_rr=width_rr,
|
||||
width_state=width_state))
|
||||
|
||||
print("")
|
||||
|
||||
if 'info' in data and data["info"] != "":
|
||||
print(data["info"])
|
||||
|
||||
if 'warning' in data and data["warning"] != "":
|
||||
print("Warning:")
|
||||
print(data["warning"])
|
||||
|
||||
if 'error' in data and data["error"] != "":
|
||||
print("Error:")
|
||||
print(data["error"])
|
||||
|
||||
elif resp.status_code == 500:
|
||||
print("An internal error has occurred. Please check /var/log/software.log for details")
|
||||
else:
|
||||
# print("Error: %s has occurred. %s" % (resp.status_code, resp.reason))
|
||||
print("Error: %s has occurred." % (resp.status_code))
|
||||
|
||||
|
||||
def print_result_debug(req, data):
|
||||
if req.status_code == 200:
|
||||
if 'sd' in data:
|
||||
print(json.dumps(data['sd'],
|
||||
sort_keys=True,
|
||||
indent=4,
|
||||
separators=(',', ': ')))
|
||||
elif 'data' in data:
|
||||
print(json.dumps(data['data'],
|
||||
sort_keys=True,
|
||||
indent=4,
|
||||
separators=(',', ': ')))
|
||||
else:
|
||||
print(json.dumps(data,
|
||||
sort_keys=True,
|
||||
indent=4,
|
||||
separators=(',', ': ')))
|
||||
elif req.status_code == 500:
|
||||
print("An internal error has occurred. Please check /var/log/software.log for details")
|
||||
else:
|
||||
m = re.search("(Error message:.*)", data, re.MULTILINE)
|
||||
if m:
|
||||
print(m.group(0))
|
||||
else:
|
||||
print("%s %s" % (req.status_code, req.reason))
|
@ -1,9 +1,9 @@
|
||||
"""
|
||||
Copyright (c) 2023-2024 Wind River Systems, Inc.
|
||||
#
|
||||
# Copyright (c) 2023-2024 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
ADDRESS_VERSION_IPV4 = 4
|
||||
ADDRESS_VERSION_IPV6 = 6
|
||||
CONTROLLER_FLOATING_HOSTNAME = "controller"
|
||||
@ -64,3 +64,8 @@ SCRATCH_DIR = "/scratch"
|
||||
DEPLOYING = 'deploying'
|
||||
FAILED = 'failed'
|
||||
PENDING = 'pending'
|
||||
|
||||
# Authorization modes of software cli
|
||||
KEYSTONE = 'keystone'
|
||||
TOKEN = 'token'
|
||||
LOCAL_ROOT = 'local_root'
|
||||
|
126
software-client/software_client/exc.py
Normal file
126
software-client/software_client/exc.py
Normal file
@ -0,0 +1,126 @@
|
||||
# Copyright 2013-2024 Wind River, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import six
|
||||
import sys
|
||||
|
||||
|
||||
class BaseException(Exception):
|
||||
"""An error occurred."""
|
||||
def __init__(self, message=None):
|
||||
super(BaseException, self).__init__()
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return str(self.message) or self.__class__.__doc__
|
||||
|
||||
|
||||
class CommandError(BaseException):
|
||||
"""Invalid usage of CLI."""
|
||||
|
||||
|
||||
class InvalidEndpoint(BaseException):
|
||||
"""The provided endpoint is invalid."""
|
||||
|
||||
|
||||
class CommunicationError(BaseException):
|
||||
"""Unable to communicate with server."""
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""DEPRECATED."""
|
||||
|
||||
|
||||
class NoTokenLookupException(Exception):
|
||||
"""DEPRECATED."""
|
||||
pass # pylint: disable=unnecessary-pass
|
||||
|
||||
|
||||
class EndpointNotFound(Exception):
|
||||
"""DEPRECATED."""
|
||||
pass # pylint: disable=unnecessary-pass
|
||||
|
||||
|
||||
class AmbiguousAuthSystem(ClientException):
|
||||
"""Could not obtain token and endpoint using provided credentials."""
|
||||
pass # pylint: disable=unnecessary-pass
|
||||
|
||||
|
||||
class SoftwareclientException(Exception):
|
||||
"""Base Software-Client Exception
|
||||
|
||||
To correctly use this class, inherit from it and define
|
||||
a 'message' property. That message will get printf'd
|
||||
with the keyword arguments provided to the constructor.
|
||||
|
||||
"""
|
||||
message = "An unknown exception occurred."
|
||||
code = 500
|
||||
headers = {}
|
||||
safe = False
|
||||
|
||||
def __init__(self, message=None, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
if 'code' not in self.kwargs:
|
||||
try:
|
||||
self.kwargs['code'] = self.code
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if not message:
|
||||
try:
|
||||
message = self.message % kwargs # pylint: disable=exception-message-attribute
|
||||
|
||||
except Exception:
|
||||
# kwargs doesn't match a variable in the message
|
||||
# at least get the core message out if something happened
|
||||
message = self.message # pylint: disable=exception-message-attribute
|
||||
|
||||
super(SoftwareclientException, self).__init__(message)
|
||||
|
||||
def format_message(self):
|
||||
if self.__class__.__name__.endswith('_Remote'):
|
||||
return self.args[0] # pylint: disable=unsubscriptable-object
|
||||
else:
|
||||
return six.text_type(self)
|
||||
|
||||
|
||||
class AmbiguousEndpoints(SoftwareclientException):
|
||||
message = "Endpoints are ambiguous. reason=%(reason)s"
|
||||
|
||||
|
||||
class EndpointTypeNotFound(SoftwareclientException):
|
||||
message = "The type of the endpoint was not found. reason=%(reason)s"
|
||||
|
||||
|
||||
class SslCertificateValidationError(SoftwareclientException):
|
||||
message = "Validation of the Ssl certificate failed. reason=%(reason)s"
|
||||
|
||||
|
||||
class EndpointException(SoftwareclientException):
|
||||
message = "Generic endpoint exception. reason=%(reason)s"
|
||||
|
||||
|
||||
# Alias for backwards compatibility
|
||||
AmbigiousAuthSystem = AmbiguousAuthSystem
|
||||
|
||||
|
||||
class InvalidAttribute(ClientException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAttributeValue(ClientException):
|
||||
pass
|
File diff suppressed because it is too large
Load Diff
106
software-client/software_client/tests/test_shell.py
Normal file
106
software-client/software_client/tests/test_shell.py
Normal file
@ -0,0 +1,106 @@
|
||||
#
|
||||
# Copyright (c) 2013-2024 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import fixtures
|
||||
import httplib2
|
||||
from unittest import mock
|
||||
import re
|
||||
from six.moves import cStringIO as StringIO
|
||||
import sys
|
||||
from testtools import matchers
|
||||
|
||||
import keystoneauth1
|
||||
|
||||
from software_client import exc
|
||||
from software_client import software_client
|
||||
from software_client.tests import utils
|
||||
|
||||
FAKE_ENV = {'OS_USERNAME': 'username',
|
||||
'OS_PASSWORD': 'password',
|
||||
'OS_PROJECT_NAME': 'project',
|
||||
'OS_REGION_NAME': 'region',
|
||||
'OS_TENANT_NAME': 'tenant_name',
|
||||
'OS_AUTH_URL': 'http://no.where'}
|
||||
|
||||
|
||||
class ShellTest(utils.BaseTestCase):
|
||||
re_options = re.DOTALL | re.MULTILINE
|
||||
|
||||
mock_endpoint_patcher = mock.patch.object(keystoneauth1.session.Session,
|
||||
'get_endpoint')
|
||||
|
||||
# Patch os.environ to avoid required auth info.
|
||||
def make_env(self, exclude=None):
|
||||
env = dict((k, v) for k, v in FAKE_ENV.items() if k != exclude)
|
||||
self.useFixture(fixtures.MonkeyPatch('os.environ', env))
|
||||
|
||||
def setUp(self):
|
||||
super(ShellTest, self).setUp()
|
||||
self.mock_endpoint = self.mock_endpoint_patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
super(ShellTest, self).tearDown()
|
||||
self.mock_endpoint_patcher.stop()
|
||||
|
||||
def shell(self, argstr):
|
||||
orig = sys.stdout
|
||||
try:
|
||||
sys.stdout = StringIO()
|
||||
_shell = software_client.SoftwareClientShell()
|
||||
_shell.main(argstr.split())
|
||||
except SystemExit:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
self.assertEqual(exc_value.code, 0)
|
||||
finally:
|
||||
out = sys.stdout.getvalue()
|
||||
sys.stdout.close()
|
||||
sys.stdout = orig
|
||||
|
||||
return out
|
||||
|
||||
def test_help_unknown_command(self):
|
||||
self.assertRaises(exc.CommandError, self.shell, 'help foofoo')
|
||||
|
||||
def test_debug(self):
|
||||
httplib2.debuglevel = 0
|
||||
self.shell('--debug help')
|
||||
self.assertEqual(httplib2.debuglevel, 1)
|
||||
|
||||
def test_help(self):
|
||||
required = [
|
||||
'.*?^usage: software',
|
||||
'.*?^See "software help COMMAND" '
|
||||
'for help on a specific command',
|
||||
]
|
||||
for argstr in ['--help', 'help']:
|
||||
help_text = self.shell(argstr)
|
||||
for r in required:
|
||||
self.assertThat(help_text,
|
||||
matchers.MatchesRegex(r,
|
||||
self.re_options))
|
||||
|
||||
def test_help_on_subcommand(self):
|
||||
required = [
|
||||
r'.*?^usage: software list \[--release RELEASE\] \[--state STATE\]'
|
||||
r'',
|
||||
r".*?^List the software releases",
|
||||
r'',
|
||||
r".*?^Optional arguments:",
|
||||
r".*?--release RELEASE filter against a release ID",
|
||||
r".*?--state STATE filter against a release state",
|
||||
]
|
||||
argstrings = [
|
||||
'help list',
|
||||
]
|
||||
for argstr in argstrings:
|
||||
help_text = self.shell(argstr)
|
||||
for r in required:
|
||||
self.assertThat(help_text,
|
||||
matchers.MatchesRegex(r, self.re_options))
|
||||
|
||||
def test_auth_param(self):
|
||||
self.make_env(exclude='OS_USERNAME')
|
||||
self.test_help()
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright (c) 2023 Wind River Systems, Inc.
|
||||
# Copyright (c) 2023-2024 Wind River Systems, Inc.
|
||||
#
|
||||
|
||||
import json
|
||||
@ -138,8 +138,7 @@ class SoftwareClientHelpTestCase(SoftwareClientTestCase, SoftwareClientNonRootMi
|
||||
"""When no arguments are called, it should call print_usage"""
|
||||
shell_args = [self.PROG, ]
|
||||
self._test_method(shell_args=shell_args)
|
||||
mock_usage.assert_called()
|
||||
mock_help.assert_not_called()
|
||||
mock_help.assert_called()
|
||||
mock_check.assert_not_called()
|
||||
|
||||
@mock.patch('software_client.software_client.check_for_os_region_name')
|
||||
@ -149,7 +148,6 @@ class SoftwareClientHelpTestCase(SoftwareClientTestCase, SoftwareClientNonRootMi
|
||||
"""When -h is passed in, this should invoke print_help"""
|
||||
shell_args = [self.PROG, "-h"]
|
||||
self._test_method(shell_args=shell_args)
|
||||
mock_usage.assert_not_called()
|
||||
mock_help.assert_called()
|
||||
mock_check.assert_not_called()
|
||||
|
||||
@ -160,6 +158,5 @@ class SoftwareClientHelpTestCase(SoftwareClientTestCase, SoftwareClientNonRootMi
|
||||
"""invalid args should invoke print_usage"""
|
||||
shell_args = [self.PROG, "invalid_arg"]
|
||||
self._test_method(shell_args=shell_args)
|
||||
mock_usage.assert_called()
|
||||
mock_help.assert_not_called()
|
||||
mock_check.assert_not_called()
|
||||
|
84
software-client/software_client/tests/utils.py
Normal file
84
software-client/software_client/tests/utils.py
Normal file
@ -0,0 +1,84 @@
|
||||
# Copyright 2024 Wind River Systems, Inc.
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import fixtures
|
||||
import testtools
|
||||
|
||||
from software_client.common import http
|
||||
from six import StringIO
|
||||
|
||||
|
||||
class BaseTestCase(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(BaseTestCase, self).setUp()
|
||||
self.useFixture(fixtures.FakeLogger())
|
||||
|
||||
|
||||
class FakeAPI(object):
|
||||
def __init__(self, fixtures):
|
||||
self.fixtures = fixtures
|
||||
self.calls = []
|
||||
|
||||
def _request(self, method, url, headers=None, body=None):
|
||||
call = (method, url, headers or {}, body)
|
||||
self.calls.append(call)
|
||||
return self.fixtures[url][method]
|
||||
|
||||
def raw_request(self, *args, **kwargs):
|
||||
fixture = self._request(*args, **kwargs)
|
||||
body_iter = http.ResponseBodyIterator(StringIO(fixture[1]))
|
||||
return FakeResponse(fixture[0]), body_iter
|
||||
|
||||
def json_request(self, *args, **kwargs):
|
||||
fixture = self._request(*args, **kwargs)
|
||||
return FakeResponse(fixture[0]), fixture[1]
|
||||
|
||||
def upload_request_with_multipart(self, *args, **kwargs):
|
||||
# TODO(gdossant): add 'data' parameter to _request method.
|
||||
# It will impact more than 40 tests and must be done in
|
||||
# a specific commit.
|
||||
|
||||
kwargs.pop('check_exceptions')
|
||||
data = kwargs.pop('data')
|
||||
|
||||
fixture = self._request(*args, **kwargs)
|
||||
|
||||
call = list(self.calls[0])
|
||||
call.append(data)
|
||||
self.calls[0] = tuple(call)
|
||||
|
||||
return fixture[1]
|
||||
|
||||
|
||||
class FakeResponse(object):
|
||||
def __init__(self, headers, body=None, version=None):
|
||||
""":param headers: dict representing HTTP response headers
|
||||
:param body: file-like object
|
||||
"""
|
||||
self.headers = headers
|
||||
self.body = body
|
||||
self.status_code = 200
|
||||
|
||||
def getheaders(self):
|
||||
return copy.deepcopy(self.headers).items()
|
||||
|
||||
def getheader(self, key, default):
|
||||
return self.headers.get(key, default)
|
||||
|
||||
def read(self, amt):
|
||||
return self.body.read(amt)
|
180
software-client/software_client/tests/v1/test_deploy.py
Normal file
180
software-client/software_client/tests/v1/test_deploy.py
Normal file
@ -0,0 +1,180 @@
|
||||
# Copyright (c) 2019-2024 Wind River Systems, Inc.
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import testtools
|
||||
|
||||
from software_client.tests import utils
|
||||
import software_client.v1.deploy
|
||||
import software_client.v1.deploy_shell
|
||||
|
||||
|
||||
HOST_LIST = {'data': [{
|
||||
'ip': '192.168.204.2',
|
||||
'hostname': 'controller-0',
|
||||
'deployed': True,
|
||||
'secs_since_ack': 20,
|
||||
'patch_failed': True,
|
||||
'stale_details': False,
|
||||
'latest_sysroot_commit': '95139a5067',
|
||||
'nodetype': 'controller',
|
||||
'subfunctions': ['controller', 'worker'],
|
||||
'sw_version': '24.03',
|
||||
'state': 'install-failed',
|
||||
'allow_insvc_patching': True,
|
||||
'interim_state': False,
|
||||
'reboot_required': False}]
|
||||
}
|
||||
|
||||
|
||||
fixtures = {
|
||||
'/v1/software/host_list':
|
||||
{
|
||||
'GET': (
|
||||
{},
|
||||
HOST_LIST,
|
||||
),
|
||||
},
|
||||
'/v1/software/deploy_show':
|
||||
{
|
||||
'GET': (
|
||||
{},
|
||||
{},
|
||||
),
|
||||
},
|
||||
'/v1/software/deploy_precheck/1':
|
||||
{
|
||||
'GET': (
|
||||
{},
|
||||
{},
|
||||
),
|
||||
},
|
||||
'/v1/software/deploy_precheck/1/force?region_name=RegionOne':
|
||||
{
|
||||
'POST': (
|
||||
{},
|
||||
{},
|
||||
),
|
||||
},
|
||||
'/v1/software/deploy_start/1/force':
|
||||
{
|
||||
'POST': (
|
||||
{},
|
||||
{},
|
||||
),
|
||||
},
|
||||
'/v1/software/deploy_host/1/force':
|
||||
{
|
||||
'POST': (
|
||||
{},
|
||||
{"error": True},
|
||||
),
|
||||
},
|
||||
'/v1/software/deploy_activate/1':
|
||||
{
|
||||
'POST': (
|
||||
{},
|
||||
{},
|
||||
),
|
||||
},
|
||||
'/v1/software/deploy_complete/1':
|
||||
{
|
||||
'POST': (
|
||||
{},
|
||||
{},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
class Args:
|
||||
def __init__(self, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
if isinstance(value, dict):
|
||||
self.__dict__[key] = Args(**value)
|
||||
else:
|
||||
self.__dict__[key] = value
|
||||
|
||||
|
||||
class DeployManagerTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(DeployManagerTest, self).setUp()
|
||||
self.api = utils.FakeAPI(fixtures)
|
||||
self.mgr = software_client.v1.deploy.DeployManager(self.api)
|
||||
|
||||
def test_host_list(self):
|
||||
hosts = self.mgr.host_list()
|
||||
expect = [
|
||||
('GET', '/v1/software/host_list', {}, None),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
self.assertEqual(len(hosts), 2)
|
||||
self.assertEqual(hosts[1]['data'][0]['hostname'],
|
||||
HOST_LIST['data'][0]['hostname'])
|
||||
|
||||
def test_show(self):
|
||||
deploy = self.mgr.show()
|
||||
expect = [
|
||||
('GET', '/v1/software/deploy_show', {}, None),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
|
||||
def test_precheck(self):
|
||||
input = {'deployment': '1', 'region_name': 'RegionOne', 'force': 1}
|
||||
args = Args(**input)
|
||||
check = self.mgr.precheck(args)
|
||||
expect = [
|
||||
('POST', '/v1/software/deploy_precheck/1/force?region_name=RegionOne', {}, {}),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
self.assertEqual(len(check), 2)
|
||||
|
||||
def test_start(self):
|
||||
input = {'deployment': '1', 'force': 1}
|
||||
args = Args(**input)
|
||||
resp = self.mgr.start(args)
|
||||
expect = [
|
||||
('POST', '/v1/software/deploy_start/1/force', {}, {}),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
self.assertEqual(len(resp), 2)
|
||||
|
||||
def test_host(self):
|
||||
input = {'agent': '1', 'force': 1}
|
||||
args = Args(**input)
|
||||
resp = self.mgr.host(args)
|
||||
expect = [
|
||||
('POST', '/v1/software/deploy_host/1/force', {}, {}),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
|
||||
def test_activate(self):
|
||||
input = {'deployment': '1'}
|
||||
args = Args(**input)
|
||||
resp = self.mgr.activate(args)
|
||||
expect = [
|
||||
('POST', '/v1/software/deploy_activate/1', {}, {}),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
self.assertEqual(len(resp), 2)
|
||||
|
||||
def test_complete(self):
|
||||
input = {'deployment': '1'}
|
||||
args = Args(**input)
|
||||
resp = self.mgr.complete(args)
|
||||
expect = [
|
||||
('POST', '/v1/software/deploy_complete/1', {}, {}),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
self.assertEqual(len(resp), 2)
|
196
software-client/software_client/tests/v1/test_release.py
Normal file
196
software-client/software_client/tests/v1/test_release.py
Normal file
@ -0,0 +1,196 @@
|
||||
# Copyright (c) 2013-2024 Wind River Systems, Inc.
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import testtools
|
||||
|
||||
from software_client.tests import utils
|
||||
import software_client.v1.release
|
||||
|
||||
RELEASE = {
|
||||
'sd':
|
||||
{'starlingx-24.03.0': {
|
||||
'state': 'deployed',
|
||||
'sw_version': '24.03.0',
|
||||
'status': 'REL',
|
||||
'unremovable': 'Y',
|
||||
'summary': 'STX 24.03 GA release',
|
||||
'description': 'STX 24.03 major GA release',
|
||||
'install_instructions': '',
|
||||
'warnings': '',
|
||||
'apply_active_release_only': '',
|
||||
'reboot_required': 'Y',
|
||||
'requires': [],
|
||||
'packages': []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fixtures = {
|
||||
'/v1/software/query?show=all':
|
||||
{
|
||||
'GET': (
|
||||
{},
|
||||
{'sd': RELEASE['sd']},
|
||||
),
|
||||
},
|
||||
'/v1/software/show/1':
|
||||
{
|
||||
'POST': (
|
||||
{},
|
||||
True,
|
||||
),
|
||||
},
|
||||
'/v1/software/delete/1':
|
||||
{
|
||||
'POST': (
|
||||
{},
|
||||
{},
|
||||
),
|
||||
},
|
||||
'/v1/software/is_available/1':
|
||||
{
|
||||
'POST': (
|
||||
{},
|
||||
True,
|
||||
),
|
||||
},
|
||||
'/v1/software/is_deployed/1':
|
||||
{
|
||||
'POST': (
|
||||
{},
|
||||
False,
|
||||
),
|
||||
},
|
||||
'/v1/software/is_committed/1':
|
||||
{
|
||||
'POST': (
|
||||
{},
|
||||
False,
|
||||
),
|
||||
},
|
||||
'/v1/software/install_local':
|
||||
{
|
||||
'GET': (
|
||||
{},
|
||||
{},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Args:
|
||||
def __init__(self, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
if isinstance(value, dict):
|
||||
self.__dict__[key] = Args(**value)
|
||||
else:
|
||||
self.__dict__[key] = value
|
||||
|
||||
|
||||
class ReleaseManagerTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ReleaseManagerTest, self).setUp()
|
||||
self.api = utils.FakeAPI(fixtures)
|
||||
self.mgr = software_client.v1.release.ReleaseManager(self.api)
|
||||
|
||||
def test_release_list(self):
|
||||
input = {"state": "all", "release": ""}
|
||||
args = Args(**input)
|
||||
release = self.mgr.list(args)
|
||||
expect = [
|
||||
('GET', '/v1/software/query?show=all', {}, None),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
self.assertEqual(len(release), 2)
|
||||
|
||||
def test_release_show(self):
|
||||
input = {"state": "", "release": "1"}
|
||||
args = Args(**input)
|
||||
release = self.mgr.show(args)
|
||||
expect = [
|
||||
('POST', '/v1/software/show/1', {}, {}),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
self.assertEqual(len(release), 2)
|
||||
|
||||
def test_release_delete(self):
|
||||
response = self.mgr.release_delete("1")
|
||||
expect = [
|
||||
('POST', '/v1/software/delete/1', {}, {}),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
self.assertEqual(len(response), 2)
|
||||
|
||||
def test_is_available(self):
|
||||
response = self.mgr.is_available('1')
|
||||
expect = [
|
||||
('POST', '/v1/software/is_available/1', {}, {}),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
self.assertTrue(response[1], True)
|
||||
|
||||
def test_is_deployed(self):
|
||||
response = self.mgr.is_deployed('1')
|
||||
expect = [
|
||||
('POST', '/v1/software/is_deployed/1', {}, {}),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
self.assertFalse(response[1], False)
|
||||
|
||||
def test_is_committed(self):
|
||||
response = self.mgr.is_committed('1')
|
||||
expect = [
|
||||
('POST', '/v1/software/is_committed/1', {}, {}),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
self.assertFalse(response[1], True)
|
||||
|
||||
def test_upload(self):
|
||||
input = {'release': '1', 'local': ''}
|
||||
args = Args(**input)
|
||||
response = self.mgr.upload(args)
|
||||
expect = [
|
||||
('POST', '/v1/software/upload', {}, {}),
|
||||
]
|
||||
self.assertNotEqual(self.api.calls, expect)
|
||||
self.assertEqual(response, 0)
|
||||
|
||||
def test_upload_dir(self):
|
||||
input = {'release': '1'}
|
||||
args = Args(**input)
|
||||
response = self.mgr.upload_dir(args)
|
||||
expect = [
|
||||
('POST', '/v1/software/upload', {}, {}),
|
||||
]
|
||||
self.assertNotEqual(self.api.calls, expect)
|
||||
self.assertEqual(response, 0)
|
||||
|
||||
def test_install_local(self):
|
||||
self.mgr.install_local()
|
||||
expect = [
|
||||
('GET', '/v1/software/install_local', {}, None),
|
||||
]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
|
||||
def test_commit_patch(self):
|
||||
input = {'sw_version': '1', 'all': ''}
|
||||
args = Args(**input)
|
||||
kernel = self.mgr.commit_patch(args)
|
||||
expect = [
|
||||
('GET', '/v1/software/commit_patch/1', {}, None),
|
||||
]
|
||||
self.assertNotEqual(self.api.calls, expect)
|
0
software-client/software_client/v1/__init__.py
Normal file
0
software-client/software_client/v1/__init__.py
Normal file
34
software-client/software_client/v1/client.py
Normal file
34
software-client/software_client/v1/client.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Copyright (c) 2013-2024 Wind River Systems, Inc.
|
||||
# Copyright 2012-2024 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from software_client.common import http
|
||||
from software_client.v1 import release
|
||||
from software_client.v1 import deploy
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Client for the Software v1 API.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize a new client for the Software v1 API."""
|
||||
super(Client, self).__init__()
|
||||
self.http_client = http.construct_http_client(*args, **kwargs)
|
||||
|
||||
self.release = release.ReleaseManager(self.http_client)
|
||||
self.deploy = deploy.DeployManager(self.http_client)
|
218
software-client/software_client/v1/deploy.py
Normal file
218
software-client/software_client/v1/deploy.py
Normal file
@ -0,0 +1,218 @@
|
||||
#
|
||||
# Copyright (c) 2015-2024 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import re
|
||||
import requests
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
from software_client.common import base
|
||||
from software_client.common import utils
|
||||
from software_client import constants
|
||||
|
||||
|
||||
class Deploy(base.Resource):
|
||||
def __repr__(self):
|
||||
return "<address pool %s>" % self._info
|
||||
|
||||
|
||||
class DeployManager(base.Manager):
|
||||
resource_class = Deploy
|
||||
|
||||
def precheck(self, args):
|
||||
# args.deployment is a string
|
||||
deployment = args.deployment
|
||||
|
||||
# args.region is a string
|
||||
region_name = args.region_name
|
||||
|
||||
path = "/v1/software/deploy_precheck/%s" % (deployment)
|
||||
if args.force:
|
||||
path += "/force"
|
||||
path += "?region_name=%s" % region_name
|
||||
|
||||
return self._create(path, body={})
|
||||
|
||||
def start(self, args):
|
||||
# args.deployment is a string
|
||||
deployment = args.deployment
|
||||
|
||||
# Ignore interrupts during this function
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
# Issue deploy_start request
|
||||
if args.force:
|
||||
path = "/v1/software/deploy_start/%s/force" % (deployment)
|
||||
else:
|
||||
path = "/v1/software/deploy_start/%s" % (deployment)
|
||||
|
||||
return self._create(path, body={})
|
||||
|
||||
def host(self, args):
|
||||
# args.deployment is a string
|
||||
agent_ip = args.agent
|
||||
|
||||
# Issue deploy_host request and poll for results
|
||||
path = "/v1/software/deploy_host/%s" % (agent_ip)
|
||||
|
||||
if args.force:
|
||||
path += "/force"
|
||||
|
||||
req, data = self._create(path, body={})
|
||||
if req.status_code == 200:
|
||||
if 'error' in data and data["error"] != "":
|
||||
print("Error:")
|
||||
print(data["error"])
|
||||
rc = 1
|
||||
else:
|
||||
rc = self.wait_for_install_complete(agent_ip)
|
||||
elif req.status_code == 500:
|
||||
print("An internal error has occurred. "
|
||||
"Please check /var/log/software.log for details")
|
||||
rc = 1
|
||||
else:
|
||||
m = re.search("(Error message:.*)", req.text, re.MULTILINE)
|
||||
if m:
|
||||
print(m.group(0))
|
||||
else:
|
||||
print("%s %s" % (req.status_code, req.reason))
|
||||
rc = 1
|
||||
return rc
|
||||
|
||||
def activate(self, args):
|
||||
# args.deployment is a string
|
||||
deployment = args.deployment
|
||||
|
||||
# Ignore interrupts during this function
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
# Issue deploy_start request
|
||||
path = "/v1/software/deploy_activate/%s" % (deployment)
|
||||
|
||||
return self._create(path, body={})
|
||||
|
||||
def complete(self, args):
|
||||
# args.deployment is a string
|
||||
deployment = args.deployment
|
||||
|
||||
# Ignore interrupts during this function
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
# Issue deploy_start request
|
||||
path = "/v1/software/deploy_complete/%s" % (deployment)
|
||||
|
||||
return self._create(path, body={})
|
||||
|
||||
def host_list(self):
|
||||
path = '/v1/software/host_list'
|
||||
return self._list(path, "")
|
||||
|
||||
def show(self):
|
||||
path = '/v1/software/deploy'
|
||||
req, data = self._list(path, "")
|
||||
|
||||
if req.status_code >= 500:
|
||||
print("An internal error has occurred. Please check /var/log/software.log for details")
|
||||
return 1
|
||||
elif req.status_code >= 400:
|
||||
print("Respond code %d. Error: %s" % (req.status_code, req.reason))
|
||||
return 1
|
||||
|
||||
if not data or data.get("data"):
|
||||
print("No deploy in progress.")
|
||||
else:
|
||||
data = data.get("data")
|
||||
data = data[0]
|
||||
data["reboot_required"] = "Yes" if data.get("reboot_required") else "No"
|
||||
data_list = [[k, v] for k, v in data.items()]
|
||||
transposed_data_list = list(zip(*data_list))
|
||||
|
||||
transposed_data_list[0] = [s.title().replace('_', ' ') for s in transposed_data_list[0]]
|
||||
# Find the longest header string in each column
|
||||
header_lengths = [len(str(x)) for x in transposed_data_list[0]]
|
||||
# Find the longest content string in each column
|
||||
content_lengths = [len(str(x)) for x in transposed_data_list[1]]
|
||||
# Find the max of the two for each column
|
||||
col_lengths = [(x if x > y else y) for x, y in zip(header_lengths, content_lengths)]
|
||||
|
||||
print(' '.join(f"{x.center(col_lengths[i])}" for i,
|
||||
x in enumerate(transposed_data_list[0])))
|
||||
print(' '.join('=' * length for length in col_lengths))
|
||||
print(' '.join(f"{x.center(col_lengths[i])}" for i,
|
||||
x in enumerate(transposed_data_list[1])))
|
||||
|
||||
return 0
|
||||
|
||||
def wait_for_install_complete(self, agent_ip):
|
||||
url = "/v1/software/host_list"
|
||||
rc = 0
|
||||
|
||||
max_retries = 4
|
||||
retriable_count = 0
|
||||
|
||||
while True:
|
||||
# Sleep on the first pass as well, to allow time for the
|
||||
# agent to respond
|
||||
time.sleep(5)
|
||||
|
||||
try:
|
||||
req, data = self._list(url)
|
||||
except requests.exceptions.ConnectionError:
|
||||
# The local software-controller may have restarted.
|
||||
retriable_count += 1
|
||||
if retriable_count <= max_retries:
|
||||
continue
|
||||
else:
|
||||
print("Lost communications with the software controller")
|
||||
rc = 1
|
||||
break
|
||||
|
||||
if req.status_code == 200:
|
||||
data = data.get("data", None)
|
||||
if not data:
|
||||
print("Invalid host-list data returned:")
|
||||
utils.print_result_debug(req, data)
|
||||
rc = 1
|
||||
break
|
||||
|
||||
host_state = None
|
||||
|
||||
for d in data:
|
||||
if d['hostname'] == agent_ip:
|
||||
host_state = d.get('host_state')
|
||||
|
||||
if host_state == constants.DEPLOYING:
|
||||
# Still deploying
|
||||
sys.stdout.write(".")
|
||||
sys.stdout.flush()
|
||||
elif host_state == constants.FAILED:
|
||||
print("\nDeployment failed. Please check logs for details.")
|
||||
rc = 1
|
||||
break
|
||||
elif host_state == constants.DEPLOYED:
|
||||
print("\nDeployment was successful.")
|
||||
rc = 0
|
||||
break
|
||||
else:
|
||||
print("\nReported unknown state: %s" % host_state)
|
||||
rc = 1
|
||||
break
|
||||
|
||||
elif req.status_code == 500:
|
||||
print("An internal error has occurred. Please check /var/log/software.log for details")
|
||||
rc = 1
|
||||
break
|
||||
else:
|
||||
m = re.search("(Error message:.*)", req.text, re.MULTILINE)
|
||||
if m:
|
||||
print(m.group(0))
|
||||
else:
|
||||
print(vars(req))
|
||||
rc = 1
|
||||
break
|
||||
|
||||
return rc
|
41
software-client/software_client/v1/deploy_cmd.py
Normal file
41
software-client/software_client/v1/deploy_cmd.py
Normal file
@ -0,0 +1,41 @@
|
||||
#
|
||||
# Copyright (c) 2013-2024 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
#
|
||||
|
||||
from software_client.common import utils
|
||||
from software_client.v1 import deploy_shell
|
||||
|
||||
|
||||
DEPLOY_COMMAND_MODULES = [
|
||||
deploy_shell,
|
||||
]
|
||||
|
||||
# sofware deploy commands
|
||||
# - precheck
|
||||
# - start
|
||||
# - host
|
||||
# - activate
|
||||
# - complete
|
||||
# non root/sudo users can run:
|
||||
# - host-list
|
||||
# - show
|
||||
# Deploy commands are region_restricted, which means
|
||||
# that they are not permitted to be run in DC
|
||||
#
|
||||
# UN_RESTRICTED_COMMANDS is used to set argparser argument 'restricted' to False
|
||||
UN_RESTRICTED_COMMANDS = ['show', 'host-list']
|
||||
|
||||
|
||||
def enhance_parser(parser, subparsers, cmd_mapper):
|
||||
'''Take a basic (nonversioned) parser and enhance it with
|
||||
commands and options specific for this version of API.
|
||||
|
||||
:param parser: top level parser :param subparsers: top level
|
||||
parser's subparsers collection where subcommands will go
|
||||
'''
|
||||
for command_module in DEPLOY_COMMAND_MODULES:
|
||||
utils.define_commands_from_module(subparsers, command_module,
|
||||
cmd_mapper, UN_RESTRICTED_COMMANDS)
|
112
software-client/software_client/v1/deploy_shell.py
Normal file
112
software-client/software_client/v1/deploy_shell.py
Normal file
@ -0,0 +1,112 @@
|
||||
#
|
||||
# Copyright (c) 2024 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
from software_client.common import utils
|
||||
|
||||
|
||||
# --deployment is an optional argument
|
||||
@utils.arg('--deployment',
|
||||
required=False,
|
||||
help='List the deployment specified')
|
||||
# --state is an optional argument.
|
||||
# default: "all"
|
||||
# acceptable values: inactive, active, prestaging, prestaged, all
|
||||
@utils.arg('--state',
|
||||
choices=['inactive', 'active', 'prestaging', 'prestaged', 'all'],
|
||||
default="all",
|
||||
required=False,
|
||||
help="List all deployments that have this state")
|
||||
def do_show(cc, args):
|
||||
"""Show the software deployments states"""
|
||||
return cc.deploy.show()
|
||||
|
||||
|
||||
def do_host_list(cc, args):
|
||||
"""List of hosts for software deployment """
|
||||
req, data = cc.deploy.host_list()
|
||||
if args.debug:
|
||||
utils.print_result_debug(req, data)
|
||||
else:
|
||||
utils.print_software_deploy_host_list_result(req, data)
|
||||
|
||||
return utils.check_rc(req, data)
|
||||
|
||||
|
||||
@utils.arg('deployment',
|
||||
help='Verify if prerequisites are met for this Deployment ID')
|
||||
@utils.arg('-f',
|
||||
'--force',
|
||||
action='store_true',
|
||||
required=False,
|
||||
help='Allow bypassing non-critical checks')
|
||||
@utils.arg('--region_name',
|
||||
default='RegionOne',
|
||||
required=False,
|
||||
help='Run precheck against a subcloud')
|
||||
def do_precheck(cc, args):
|
||||
"""Verify whether prerequisites for installing the software deployment are satisfied"""
|
||||
req, data = cc.deploy.precheck(args)
|
||||
if args.debug:
|
||||
utils.print_result_debug(req, data)
|
||||
else:
|
||||
utils.print_software_op_result(req, data)
|
||||
|
||||
return utils.check_rc(req, data)
|
||||
|
||||
|
||||
@utils.arg('deployment',
|
||||
help='Deployment ID to start')
|
||||
@utils.arg('-f',
|
||||
'--force',
|
||||
action='store_true',
|
||||
required=False,
|
||||
help='Allow bypassing non-critical checks')
|
||||
def do_start(cc, args):
|
||||
"""Start the software deployment"""
|
||||
req, data = cc.deploy.start(args)
|
||||
if args.debug:
|
||||
utils.print_result_debug(req, data)
|
||||
else:
|
||||
utils.print_software_op_result(req, data)
|
||||
|
||||
return utils.check_rc(req, data)
|
||||
|
||||
|
||||
@utils.arg('agent',
|
||||
help="Agent on which host deploy is triggered")
|
||||
@utils.arg('-f',
|
||||
'--force',
|
||||
action='store_true',
|
||||
required=False,
|
||||
help="Force deploy host")
|
||||
def do_host(cc, args):
|
||||
"""Deploy prestaged software deployment to the host"""
|
||||
return cc.deploy.host(args)
|
||||
|
||||
|
||||
@utils.arg('deployment',
|
||||
help='Deployment ID to activate')
|
||||
def do_activate(cc, args):
|
||||
"""Activate the software deployment"""
|
||||
req, data = cc.deploy.activate(args)
|
||||
if args.debug:
|
||||
utils.print_result_debug(req, data)
|
||||
else:
|
||||
utils.print_software_op_result(req, data)
|
||||
|
||||
return utils.check_rc(req, data)
|
||||
|
||||
@utils.arg('deployment',
|
||||
help='Deployment ID to complete')
|
||||
def do_complete(cc, args):
|
||||
"""Complete the software deployment"""
|
||||
req, data = cc.deploy.complete(args)
|
||||
if args.debug:
|
||||
utils.print_result_debug(req, data)
|
||||
else:
|
||||
utils.print_software_op_result(req, data)
|
||||
|
||||
return utils.check_rc(req, data)
|
266
software-client/software_client/v1/release.py
Normal file
266
software-client/software_client/v1/release.py
Normal file
@ -0,0 +1,266 @@
|
||||
#
|
||||
# Copyright (c) 2015-2024 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
from requests_toolbelt import MultipartEncoder
|
||||
from tsconfig.tsconfig import SW_VERSION as RUNNING_SW_VERSION
|
||||
|
||||
from software_client.common import base
|
||||
from software_client.common import utils
|
||||
from software_client import constants
|
||||
|
||||
|
||||
class Release(base.Resource):
|
||||
def __repr__(self):
|
||||
return "<release %s>" % self._info
|
||||
|
||||
|
||||
class ReleaseManager(base.Manager):
|
||||
resource_class = Release
|
||||
|
||||
def list(self, args):
|
||||
state = args.state # defaults to "all"
|
||||
extra_opts = ""
|
||||
if args.release:
|
||||
extra_opts = "&release=%s" % args.release
|
||||
path = "/v1/software/query?show=%s%s" % (state, extra_opts)
|
||||
return self._list(path, "")
|
||||
|
||||
def is_available(self, release):
|
||||
releases = "/".join(release)
|
||||
path = '/v1/software/is_available/%s' % (releases)
|
||||
return self._create(path, body={})
|
||||
|
||||
def is_deployed(self, release):
|
||||
releases = "/".join(release)
|
||||
path = '/v1/software/is_deployed/%s' % (releases)
|
||||
return self._create(path, body={})
|
||||
|
||||
def is_committed(self, release):
|
||||
releases = "/".join(release)
|
||||
path = '/v1/software/is_committed/%s' % (releases)
|
||||
return self._create(path, body={})
|
||||
|
||||
def upload(self, args):
|
||||
rc = 0
|
||||
|
||||
# arg.release is a list
|
||||
releases = args.release
|
||||
is_local = args.local # defaults to False
|
||||
|
||||
# Ignore interrupts during this function
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
to_upload_files = {}
|
||||
valid_files = []
|
||||
invalid_files = []
|
||||
|
||||
# Validate all the files
|
||||
valid_files = [os.path.abspath(software_file) for software_file in releases if os.path.isfile(
|
||||
software_file) and os.path.splitext(software_file)[1] in constants.SUPPORTED_UPLOAD_FILE_EXT]
|
||||
invalid_files = [os.path.abspath(software_file) for software_file in releases
|
||||
if os.path.abspath(software_file) not in valid_files]
|
||||
|
||||
for software_file in invalid_files:
|
||||
if os.path.isdir(software_file):
|
||||
print("Error: %s is a directory. Please use upload-dir" % software_file)
|
||||
elif os.path.isfile(software_file):
|
||||
print("Error: %s has the unsupported file extension." % software_file)
|
||||
else:
|
||||
print("Error: File does not exist: %s" % software_file)
|
||||
|
||||
if len(valid_files) == 0:
|
||||
print("No file to be uploaded.")
|
||||
return rc
|
||||
|
||||
path = '/v1/software/upload'
|
||||
if is_local:
|
||||
to_upload_filenames = json.dumps(valid_files)
|
||||
headers = {'Content-Type': 'text/plain'}
|
||||
return self._create(path, body=to_upload_filenames, headers=headers)
|
||||
else:
|
||||
for software_file in valid_files:
|
||||
with open(software_file, 'rb') as file:
|
||||
data_content = file.read()
|
||||
to_upload_files[software_file] = (software_file, data_content)
|
||||
|
||||
encoder = MultipartEncoder(fields=to_upload_files)
|
||||
headers = {'Content-Type': encoder.content_type}
|
||||
return self._create_multipart(path, body=encoder, headers=headers)
|
||||
|
||||
def upload_dir(self, args):
|
||||
# arg.release is a list
|
||||
release_dirs = args.release
|
||||
|
||||
# Ignore interrupts during this function
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
to_upload_files = {}
|
||||
raw_files = []
|
||||
|
||||
# Find all files that need to be uploaded in given directories
|
||||
for release_dir in release_dirs:
|
||||
if os.path.isdir(release_dir):
|
||||
raw_files = [f for f in os.listdir(release_dir)
|
||||
if os.path.isfile(os.path.join(release_dir, f))]
|
||||
|
||||
# Get absolute path of files
|
||||
raw_files = [os.path.abspath(os.path.join(release_dir, f)) for f in raw_files]
|
||||
else:
|
||||
print("Skipping invalid directory: %s" % release_dir, file=sys.stderr)
|
||||
|
||||
if len(raw_files) == 0:
|
||||
print("No file to upload")
|
||||
return 0
|
||||
|
||||
temp_iso_files = [f for f in raw_files if f.endswith(constants.ISO_EXTENSION)]
|
||||
if len(temp_iso_files) > 1: # Verify that only one ISO file is being uploaded
|
||||
print("Only one ISO file can be uploaded at a time. Found: %s" %
|
||||
temp_iso_files, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
temp_sig_files = [f for f in raw_files if f.endswith(constants.SIG_EXTENSION)]
|
||||
if len(temp_sig_files) > 1: # Verify that only one SIG file is being uploaded
|
||||
print("Only one SIG file can be uploaded at a time. Found: %s" %
|
||||
temp_sig_files, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
for software_file in sorted(set(raw_files)):
|
||||
_, ext = os.path.splitext(software_file)
|
||||
if ext in constants.SUPPORTED_UPLOAD_FILE_EXT:
|
||||
to_upload_files[software_file] = (software_file, open(software_file, 'rb'))
|
||||
|
||||
encoder = MultipartEncoder(fields=to_upload_files)
|
||||
headers = {'Content-Type': encoder.content_type}
|
||||
path = '/v1/software/upload'
|
||||
req, data = self._create_multipart(path, body=encoder, headers=headers)
|
||||
if args.debug:
|
||||
utils.print_result_debug(req, data)
|
||||
else:
|
||||
utils.print_software_op_result(req, data)
|
||||
return utils.check_rc(req, data)
|
||||
|
||||
def commit_patch(self, args):
|
||||
# Ignore interrupts during this function
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
# Default to running release
|
||||
# this all needs to be changed
|
||||
relopt = RUNNING_SW_VERSION
|
||||
|
||||
# append_auth_token_if_required(headers)
|
||||
if args.sw_version and not args.all:
|
||||
# Disallow
|
||||
print("Use of --sw-version option requires --all")
|
||||
return 1
|
||||
elif args.all:
|
||||
# Get a list of all patches
|
||||
extra_opts = "&release=%s" % relopt
|
||||
url = "/v1/software/query?show=patch%s" % (extra_opts)
|
||||
|
||||
resp, body = self._list(url, "")
|
||||
|
||||
patch_list = []
|
||||
if resp.status_code == 200:
|
||||
data = body
|
||||
|
||||
if 'sd' in data:
|
||||
patch_list = sorted(list(data['sd']))
|
||||
elif resp.status_code == 500:
|
||||
print("Failed to get patch list. Aborting...")
|
||||
return 1
|
||||
|
||||
if len(patch_list) == 0:
|
||||
print("There are no %s patches to commit." % relopt)
|
||||
return 0
|
||||
|
||||
print("The following patches will be committed:")
|
||||
for patch_id in patch_list:
|
||||
print(" %s" % patch_id)
|
||||
print()
|
||||
|
||||
patches = "/".join(patch_list)
|
||||
else:
|
||||
# args.patch is a list
|
||||
patches = "/".join(args.patch)
|
||||
|
||||
# First, get a list of dependencies and ask for confirmation
|
||||
url = "/v1/software/query_dependencies/%s?recursive=yes" % (patches)
|
||||
|
||||
resp, body = self._list(url, "")
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = body
|
||||
|
||||
if 'patches' in data:
|
||||
print("The following patches will be committed:")
|
||||
for release_id in sorted(data['patches']):
|
||||
print(" %s" % release_id)
|
||||
print()
|
||||
else:
|
||||
print("No patches found to commit")
|
||||
return 1
|
||||
|
||||
elif resp.status_code == 500:
|
||||
print("An internal error has occurred. Please check /var/log/software.log for details")
|
||||
return 1
|
||||
|
||||
# Run dry-run
|
||||
url = "/v1/software/commit_dry_run/%s" % (patches)
|
||||
|
||||
resp, body = self._create(url, body={})
|
||||
utils.print_software_op_result(resp, body)
|
||||
|
||||
if utils.check_rc(resp, body) != 0:
|
||||
print("Aborting...")
|
||||
return 1
|
||||
|
||||
if args.dry_run:
|
||||
return 0
|
||||
|
||||
print()
|
||||
commit_warning = "WARNING: Committing a patch is an irreversible operation. " + \
|
||||
"Committed patches cannot be removed."
|
||||
print(textwrap.fill(commit_warning, width=utils.TERM_WIDTH, subsequent_indent=' ' * 9))
|
||||
print()
|
||||
|
||||
user_input = input("Would you like to continue? [y/N]: ")
|
||||
if user_input.lower() != 'y':
|
||||
print("Aborting...")
|
||||
return 1
|
||||
|
||||
url = "/v1/software/commit_patch/%s" % (patches)
|
||||
req = self._create(url, body={})
|
||||
|
||||
if args.debug:
|
||||
utils.print_result_debug(req)
|
||||
else:
|
||||
utils.print_software_op_result(req)
|
||||
return
|
||||
|
||||
def install_local(self):
|
||||
# Ignore interrupts during this function
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
path = "/v1/software/install_local"
|
||||
return self._list(path, "")
|
||||
|
||||
def show(self, args):
|
||||
releases = "/".join(args.release)
|
||||
|
||||
path = "/v1/software/show/%s" % (releases)
|
||||
return self._create(path, body={})
|
||||
|
||||
def release_delete(self, release_id):
|
||||
release_ids = "/".join(release_id)
|
||||
path = '/v1/software/delete/%s' % release_ids
|
||||
return self._create(path, body={})
|
196
software-client/software_client/v1/release_shell.py
Normal file
196
software-client/software_client/v1/release_shell.py
Normal file
@ -0,0 +1,196 @@
|
||||
#
|
||||
# Copyright (c) 2015-2024 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
from software_client.common import utils
|
||||
|
||||
|
||||
# --release is an optional argument
|
||||
@utils.arg('--release',
|
||||
required=False,
|
||||
help='filter against a release ID')
|
||||
# --state is an optional argument. default: "all"
|
||||
@utils.arg('--state',
|
||||
default="all",
|
||||
required=False,
|
||||
help='filter against a release state')
|
||||
def do_list(cc, args):
|
||||
"""List the software releases"""
|
||||
req, data = cc.release.list(args)
|
||||
if args.debug:
|
||||
utils.print_result_debug(req, data)
|
||||
else:
|
||||
header_data_list = ["Release", "RR", "State"]
|
||||
data_list = [(k, v["reboot_required"], v["state"]) for k, v in data["sd"].items()]
|
||||
has_error = 'error' in data and data["error"]
|
||||
utils.print_result_list(header_data_list, data_list, has_error)
|
||||
|
||||
return utils.check_rc(req, data)
|
||||
|
||||
|
||||
@utils.arg('release',
|
||||
nargs="+", # accepts a list
|
||||
help='Release ID to print detailed information')
|
||||
@utils.arg('--packages',
|
||||
required=False,
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='list packages contained in the release')
|
||||
def do_show(cc, args):
|
||||
"""Show the software release"""
|
||||
list_packages = args.packages
|
||||
req, data = cc.release.show(args)
|
||||
if args.debug:
|
||||
utils.print_result_debug(req, data)
|
||||
else:
|
||||
utils.print_release_show_result(req, data, list_packages=list_packages)
|
||||
|
||||
return utils.check_rc(req, data)
|
||||
|
||||
|
||||
@utils.arg('patch',
|
||||
nargs="+", # accepts a list
|
||||
help='Patch ID/s to commit')
|
||||
# --dry-run is an optional argument
|
||||
@utils.arg('--dry-run',
|
||||
action='store_true',
|
||||
required=False,
|
||||
help='Check the space savings without committing the patch')
|
||||
# --all is an optional argument
|
||||
@utils.arg('--all',
|
||||
action='store_true',
|
||||
required=False,
|
||||
help='Commit all the applied patches')
|
||||
# --sw-version is an optional argument
|
||||
@utils.arg('--sw-version',
|
||||
required=False,
|
||||
help='Software release version')
|
||||
def do_commit_patch(cc, args):
|
||||
"""Commit patches to free disk space. WARNING: This action is irreversible!"""
|
||||
return cc.release.commit_patch(args)
|
||||
|
||||
|
||||
def do_install_local(cc, args):
|
||||
""" Trigger patch install/remove on the local host.
|
||||
This command can only be used for patch installation
|
||||
prior to initial configuration."""
|
||||
req, data = cc.release.install_local()
|
||||
if args.debug:
|
||||
utils.print_result_debug(req, data)
|
||||
else:
|
||||
utils.print_software_op_result(req, data)
|
||||
|
||||
return utils.check_rc(req, data)
|
||||
|
||||
|
||||
@utils.arg('release',
|
||||
nargs="+", # accepts a list
|
||||
help='List of releases')
|
||||
def do_is_available(cc, args):
|
||||
"""Query Available state for list of releases.
|
||||
Returns True if all are Available, False otherwise."""
|
||||
req, result = cc.release.is_available(args.release)
|
||||
rc = 1
|
||||
if req.status_code == 200:
|
||||
print(result)
|
||||
if result is True:
|
||||
rc = 0
|
||||
elif req.status_code == 500:
|
||||
print("An internal error has occurred. Please check /var/log/software.log for details")
|
||||
else:
|
||||
print("Error: %s has occurred. %s" % (req.status_code, req.reason))
|
||||
return rc
|
||||
|
||||
|
||||
@utils.arg('release',
|
||||
nargs="+", # accepts a list
|
||||
help='List of releases')
|
||||
def do_is_deployed(cc, args):
|
||||
"""Query Deployed state for list of releases.
|
||||
Returns True if all are Deployed, False otherwise."""
|
||||
req, result = cc.release.is_deployed(args.release)
|
||||
rc = 1
|
||||
if req.status_code == 200:
|
||||
print(result)
|
||||
if result is True:
|
||||
rc = 0
|
||||
elif req.status_code == 500:
|
||||
print("An internal error has occurred. Please check /var/log/software.log for details")
|
||||
else:
|
||||
print("Error: %s has occurred. %s" % (req.status_code, req.reason))
|
||||
return rc
|
||||
|
||||
|
||||
@utils.arg('release',
|
||||
nargs="+", # accepts a list
|
||||
help='List of releases')
|
||||
def do_is_committed(cc, args):
|
||||
"""Query Committed state for list of releases.
|
||||
Returns True if all are Committed, False otherwise."""
|
||||
req, result = cc.release.is_committed(args.release)
|
||||
rc = 1
|
||||
if req.status_code == 200:
|
||||
print(result)
|
||||
if result is True:
|
||||
rc = 0
|
||||
elif req.status_code == 500:
|
||||
print("An internal error has occurred. Please check /var/log/software.log for details")
|
||||
else:
|
||||
print("Error: %s has occurred. %s" % (req.status_code, req.reason))
|
||||
return rc
|
||||
|
||||
|
||||
@utils.arg('release',
|
||||
metavar='(iso + sig) | patch',
|
||||
nargs="+", # accepts a list
|
||||
help='Software releases to upload')
|
||||
@utils.arg('--local',
|
||||
required=False,
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=('pair of install iso and sig files for major release '
|
||||
'(GA or patched) and/or one or more files containing a '
|
||||
'patch release. NOTE: specify at most ONE pair of (iso + sig)'))
|
||||
def do_upload(cc, args):
|
||||
"""Upload a software release"""
|
||||
req, data = cc.release.upload(args)
|
||||
if args.debug:
|
||||
utils.print_result_debug(req, data)
|
||||
else:
|
||||
utils.print_software_op_result(req, data)
|
||||
data_list = [(k, v["id"])
|
||||
for d in data["upload_info"] for k, v in d.items()
|
||||
if not k.endswith(".sig")]
|
||||
|
||||
header_data_list = ["Uploaded File", "Id"]
|
||||
has_error = 'error' in data and data["error"]
|
||||
utils.print_result_list(header_data_list, data_list, has_error)
|
||||
rc = 0
|
||||
if utils.check_rc(req, data) != 0:
|
||||
# We hit a failure. Update rc but keep looping
|
||||
rc = 1
|
||||
return rc
|
||||
|
||||
|
||||
@utils.arg('release',
|
||||
nargs="+", # accepts a list
|
||||
help='Directory containing software releases to upload')
|
||||
def do_upload_dir(cc, args):
|
||||
"""Upload a software release dir"""
|
||||
return cc.release.upload_dir(args)
|
||||
|
||||
|
||||
@utils.arg('release',
|
||||
nargs="+", # accepts a list
|
||||
help='Release ID to delete')
|
||||
def do_delete(cc, args):
|
||||
"""Delete the software release"""
|
||||
resp, body = cc.release.release_delete(args.release)
|
||||
if args.debug:
|
||||
utils.print_result_debug(resp, body)
|
||||
else:
|
||||
utils.print_software_op_result(resp, body)
|
||||
|
||||
return utils.check_rc(resp, body)
|
26
software-client/software_client/v1/shell.py
Normal file
26
software-client/software_client/v1/shell.py
Normal file
@ -0,0 +1,26 @@
|
||||
#
|
||||
# Copyright (c) 2013-2024 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
#
|
||||
|
||||
from software_client.common import utils
|
||||
from software_client.v1 import release_shell
|
||||
|
||||
|
||||
COMMAND_MODULES = [
|
||||
release_shell,
|
||||
]
|
||||
|
||||
|
||||
def enhance_parser(parser, subparsers, cmd_mapper):
|
||||
'''Take a basic (nonversioned) parser and enhance it with
|
||||
commands and options specific for this version of API.
|
||||
|
||||
:param parser: top level parser :param subparsers: top level
|
||||
parser's subparsers collection where subcommands will go
|
||||
'''
|
||||
for command_module in COMMAND_MODULES:
|
||||
utils.define_commands_from_module(subparsers, command_module,
|
||||
cmd_mapper)
|
@ -3,5 +3,6 @@ hacking
|
||||
|
||||
bandit
|
||||
coverage
|
||||
httplib2
|
||||
pylint
|
||||
stestr
|
||||
|
Loading…
Reference in New Issue
Block a user