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:
Joseph Vazhappilly 2024-03-06 04:12:29 -05:00
parent 202751d57b
commit 0cd1d59425
24 changed files with 3423 additions and 1521 deletions

View File

@ -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"

View 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)

View 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)

View 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)

View 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))

View File

@ -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'

View 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

View 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()

View File

@ -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()

View 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)

View 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)

View 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)

View 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)

View 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

View 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)

View 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)

View 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={})

View 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)

View 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)

View File

@ -3,5 +3,6 @@ hacking
bandit
coverage
httplib2
pylint
stestr