diff --git a/.vscode/settings.json b/.vscode/settings.json index 57c96298..b378a106 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,14 @@ "python.testing.pytestArgs": ["tests"], "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, - "rust-analyzer.linkedProjects": ["./src/secrets/Cargo.toml"], + "rust-analyzer.linkedProjects": [ + "./src/secrets/Cargo.toml" + ], + "python.analysis.extraPaths": [ + "./src/core", + "./src/zos_console", + "./src/zos_files", + "./src/zos_jobs", + "./src/zosmf" + ], } diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c81886d..8bc3d0ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the Zowe Client Python SDK will be documented in this file. +## Recent Changes + +### Enhancements + +- Added logger class to core SDK [#185](https://github.com/zowe/zowe-client-python-sdk/issues/185) + ## `1.0.0-dev15` ### Bug Fixes diff --git a/src/core/zowe/core_for_zowe_sdk/__init__.py b/src/core/zowe/core_for_zowe_sdk/__init__.py index 615b966f..55bf9068 100644 --- a/src/core/zowe/core_for_zowe_sdk/__init__.py +++ b/src/core/zowe/core_for_zowe_sdk/__init__.py @@ -13,3 +13,4 @@ from .session import Session from .session_constants import * from .zosmf_profile import ZosmfProfile +from .logger import Log \ No newline at end of file diff --git a/src/core/zowe/core_for_zowe_sdk/config_file.py b/src/core/zowe/core_for_zowe_sdk/config_file.py index 581927ce..b0a4910e 100644 --- a/src/core/zowe/core_for_zowe_sdk/config_file.py +++ b/src/core/zowe/core_for_zowe_sdk/config_file.py @@ -21,11 +21,14 @@ import commentjson import requests +import logging + from .credential_manager import CredentialManager from .custom_warnings import ProfileNotFoundWarning, ProfileParsingWarning from .exceptions import ProfileNotFound from .profile_constants import GLOBAL_CONFIG_NAME, TEAM_CONFIG, USER_CONFIG from .validators import validate_config_json +from .logger import Log HOME = os.path.expanduser("~") GLOBAL_CONFIG_LOCATION = os.path.join(HOME, ".zowe") @@ -71,6 +74,8 @@ class ConfigFile: jsonc: Optional[dict] = None _missing_secure_props: list = field(default_factory=list) + __logger = Log.registerLogger(__name__) + @property def filename(self) -> str: if self.type == TEAM_CONFIG: @@ -92,20 +97,18 @@ def filepath(self) -> Optional[str]: def location(self) -> Optional[str]: return self._location - @property - def schema_path(self) -> Optional[str]: - return self.schema_property - @location.setter def location(self, dirname: str) -> None: if os.path.isdir(dirname): self._location = dirname else: + self.__logger.error(f"given path {dirname} is not valid") raise FileNotFoundError(f"given path {dirname} is not valid") def init_from_file( self, validate_schema: Optional[bool] = True, + suppress_config_file_warnings: Optional[bool] = True, ) -> None: """ Initializes the class variable after @@ -118,7 +121,9 @@ def init_from_file( pass if self.filepath is None or not os.path.isfile(self.filepath): - warnings.warn(f"Config file does not exist at {self.filepath}") + if not suppress_config_file_warnings: + self.__logger.warning(f"Config file does not exist at {self.filepath}") + warnings.warn(f"Config file does not exist at {self.filepath}") return with open(self.filepath, encoding="UTF-8", mode="r") as fileobj: @@ -129,7 +134,7 @@ def init_from_file( self.defaults = profile_jsonc.get("defaults", {}) self.jsonc = profile_jsonc - if self.schema_property and validate_schema: + if validate_schema: self.validate_schema() CredentialManager.load_secure_props() @@ -143,16 +148,11 @@ def validate_schema(self) -> None: ------- file_path to the $schema property """ - - path_schema_json = None - - path_schema_json = self.schema_path - if path_schema_json is None: # check if the $schema property is not defined - warnings.warn(f"$schema property could not found") - - # validate the $schema property - if path_schema_json: - validate_config_json(self.jsonc, path_schema_json, cwd=self.location) + if self.schema_property is None: # check if the $schema property is not defined + self.__logger.warning(f"Could not find $schema property") + warnings.warn(f"Could not find $schema property") + else: + validate_config_json(self.jsonc, self.schema_property, cwd=self.location) def schema_list(self, cwd=None) -> list: """ @@ -213,6 +213,7 @@ def get_profile( self.init_from_file(validate_schema) if profile_name is None and profile_type is None: + self.__logger.error(f"Failed to load profile: profile_name and profile_type were not provided.") raise ProfileNotFound( profile_name=profile_name, error_msg="Could not find profile as both profile_name and profile_type is not set.", @@ -250,7 +251,6 @@ def autodiscover_config_dir(self) -> None: break current_dir = os.path.dirname(current_dir) - raise FileNotFoundError(f"Could not find the file {self.filename}") def get_profilename_from_profiletype(self, profile_type: str) -> str: @@ -268,8 +268,9 @@ def get_profilename_from_profiletype(self, profile_type: str) -> str: try: profilename = self.defaults[profile_type] except KeyError: + self.__logger.warn(f"Given profile type '{profile_type}' has no default profile name") warnings.warn( - f"Given profile type '{profile_type}' has no default profilename", + f"Given profile type '{profile_type}' has no default profile name", ProfileParsingWarning, ) else: @@ -282,12 +283,14 @@ def get_profilename_from_profiletype(self, profile_type: str) -> str: if profile_type == temp_profile_type: return key except KeyError: + self.__logger.warning(f"Profile '{key}' has no type attribute") warnings.warn( f"Profile '{key}' has no type attribute", ProfileParsingWarning, ) # if no profile with matching type found, we raise an exception + self.__logger.error(f"No profile with matching profile_type '{profile_type}' found") raise ProfileNotFound( profile_name=profile_type, error_msg=f"No profile with matching profile_type '{profile_type}' found", @@ -334,6 +337,7 @@ def load_profile_properties(self, profile_name: str) -> dict: props = {**profile.get("properties", {}), **props} secure_fields.extend(profile.get("secure", [])) else: + self.__logger.warning(f"Profile {profile_name} not found") warnings.warn(f"Profile {profile_name} not found", ProfileNotFoundWarning) lst.pop() diff --git a/src/core/zowe/core_for_zowe_sdk/connection.py b/src/core/zowe/core_for_zowe_sdk/connection.py index fa97609c..41c7e553 100644 --- a/src/core/zowe/core_for_zowe_sdk/connection.py +++ b/src/core/zowe/core_for_zowe_sdk/connection.py @@ -9,7 +9,10 @@ Copyright Contributors to the Zowe Project. """ + from .exceptions import MissingConnectionArgs +from .logger import Log +import logging class ApiConnection: @@ -28,8 +31,11 @@ class ApiConnection: """ def __init__(self, host_url, user, password, ssl_verification=True): + logger = Log.registerLogger(__name__) + """Construct an ApiConnection object.""" if not host_url or not user or not password: + logger.error("Missing connection argument") raise MissingConnectionArgs() self.host_url = host_url diff --git a/src/core/zowe/core_for_zowe_sdk/credential_manager.py b/src/core/zowe/core_for_zowe_sdk/credential_manager.py index 3459619c..74787194 100644 --- a/src/core/zowe/core_for_zowe_sdk/credential_manager.py +++ b/src/core/zowe/core_for_zowe_sdk/credential_manager.py @@ -9,14 +9,18 @@ Copyright Contributors to the Zowe Project. """ + import base64 import sys from typing import Optional import commentjson +import logging + from .constants import constants from .exceptions import SecureProfileLoadFailed +from .logger import Log HAS_KEYRING = True try: @@ -27,6 +31,7 @@ class CredentialManager: secure_props = {} + __logger = Log.registerLogger(__name__) @staticmethod def load_secure_props() -> None: @@ -49,6 +54,7 @@ def load_secure_props() -> None: return except Exception as exc: + CredentialManager.__logger.error(f"Fail to load secure profile {constants['ZoweServiceName']}") raise SecureProfileLoadFailed(constants["ZoweServiceName"], error_msg=str(exc)) from exc secure_config: str @@ -75,7 +81,9 @@ def save_secure_props() -> None: if sys.platform == "win32": # Delete the existing credential CredentialManager._delete_credential(constants["ZoweServiceName"], constants["ZoweAccountName"]) - CredentialManager._set_credential(constants["ZoweServiceName"], constants["ZoweAccountName"], encoded_credential) + CredentialManager._set_credential( + constants["ZoweServiceName"], constants["ZoweAccountName"], encoded_credential + ) @staticmethod def _get_credential(service_name: str, account_name: str) -> Optional[str]: diff --git a/src/core/zowe/core_for_zowe_sdk/logger.py b/src/core/zowe/core_for_zowe_sdk/logger.py new file mode 100644 index 00000000..3549ec11 --- /dev/null +++ b/src/core/zowe/core_for_zowe_sdk/logger.py @@ -0,0 +1,29 @@ +import logging +import os + +class Log: + """root logger setup and a function to customize logger level""" + + dirname = os.path.join(os.path.expanduser("~"), ".zowe/logs") + + os.makedirs(dirname, exist_ok=True) + + logging.basicConfig( + filename=os.path.join(dirname, "python_sdk_logs.log"), + level=logging.INFO, + format="[%(asctime)s] [%(levelname)s] [%(name)s] - %(message)s", + datefmt="%m/%d/%Y %I:%M:%S %p", + ) + + loggers = [] + @staticmethod + def registerLogger(name: str): + """A function to get Logger and registered for level setting""" + logger = logging.getLogger(name) + Log.loggers.append(logger) + return logger + + @staticmethod + def setLoggerLevel(level: int): + for logger in Log.loggers: + logger.setLevel(level) diff --git a/src/core/zowe/core_for_zowe_sdk/profile_manager.py b/src/core/zowe/core_for_zowe_sdk/profile_manager.py index 7c0c78d1..0cea790d 100644 --- a/src/core/zowe/core_for_zowe_sdk/profile_manager.py +++ b/src/core/zowe/core_for_zowe_sdk/profile_manager.py @@ -15,11 +15,13 @@ import warnings from copy import deepcopy from typing import Optional +import logging import jsonschema from deepmerge import always_merger from .config_file import ConfigFile, Profile +from .logger import Log from .credential_manager import CredentialManager from .custom_warnings import ( ConfigNotFoundWarning, @@ -57,10 +59,13 @@ def __init__(self, appname: str = "zowe", show_warnings: bool = True): self.project_config = ConfigFile(type=TEAM_CONFIG, name=appname) self.project_user_config = ConfigFile(type=USER_CONFIG, name=appname) + self.__logger = Log.registerLogger(__name__) + self.global_config = ConfigFile(type=TEAM_CONFIG, name=GLOBAL_CONFIG_NAME) try: self.global_config.location = GLOBAL_CONFIG_LOCATION except Exception: + self.__logger.warning("Could not find Global Config Directory") warnings.warn( "Could not find Global Config Directory, please provide one.", ConfigNotFoundWarning, @@ -70,6 +75,7 @@ def __init__(self, appname: str = "zowe", show_warnings: bool = True): try: self.global_user_config.location = GLOBAL_CONFIG_LOCATION except Exception: + self.__logger.warning("Could not find Global User Config Directory") warnings.warn( "Could not find Global User Config Directory, please provide one.", ConfigNotFoundWarning, @@ -172,38 +178,48 @@ def get_profile( NamedTuple (data, name, secure_props_not_found) """ + logger = logging.getLogger(__name__) + cfg_profile = Profile() try: cfg_profile = cfg.get_profile( profile_name=profile_name, profile_type=profile_type, validate_schema=validate_schema ) except jsonschema.exceptions.ValidationError as exc: + logger.error(f"Instance was invalid under the provided $schema property, {exc}") raise jsonschema.exceptions.ValidationError( f"Instance was invalid under the provided $schema property, {exc}" ) except jsonschema.exceptions.SchemaError as exc: + logger.error(f"The provided schema is invalid, {exc}") raise jsonschema.exceptions.SchemaError(f"The provided schema is invalid, {exc}") except jsonschema.exceptions.UndefinedTypeCheck as exc: + logger.error(f"A type checker was asked to check a type it did not have registered, {exc}") raise jsonschema.exceptions.UndefinedTypeCheck( f"A type checker was asked to check a type it did not have registered, {exc}" ) except jsonschema.exceptions.UnknownType as exc: + logger.error(f"Unknown type is found in schema_json, {exc}") raise jsonschema.exceptions.UnknownType(f"Unknown type is found in schema_json, exc") except jsonschema.exceptions.FormatError as exc: + logger.error(f"Validating a format config_json failed for schema_json, {exc}") raise jsonschema.exceptions.FormatError(f"Validating a format config_json failed for schema_json, {exc}") except ProfileNotFound: if profile_name: + logger.warning(f"Profile '{profile_name}' not found in file '{cfg.filename}'") warnings.warn( f"Profile '{profile_name}' not found in file '{cfg.filename}', returning empty profile instead.", ProfileNotFoundWarning, ) else: + logger.warning(f"Profile of type '{profile_type}' not found in file '{cfg.filename}'") warnings.warn( f"Profile of type '{profile_type}' not found in file '{cfg.filename}', returning empty profile" f" instead.", ProfileNotFoundWarning, ) except Exception as exc: + logger.warning(f"Could not load '{cfg.filename}' at '{cfg.filepath}'" f"because {type(exc).__name__}'{exc}'") warnings.warn( f"Could not load '{cfg.filename}' at '{cfg.filepath}'" f"because {type(exc).__name__}'{exc}'.", ConfigNotFoundWarning, @@ -218,6 +234,7 @@ def load( check_missing_props: bool = True, validate_schema: Optional[bool] = True, override_with_env: Optional[bool] = False, + suppress_config_file_warnings: Optional[bool] = True, ) -> dict: """Load connection details from a team config profile. Returns @@ -236,7 +253,9 @@ def load( If `profile_type` is not base, then we will load properties from both `profile_type` and base profiles and merge them together. """ + if profile_name is None and profile_type is None: + self.__logger.error(f"Failed to load profile as both profile_name and profile_type are not set") raise ProfileNotFound( profile_name=profile_name, error_msg="Could not find profile as both profile_name and profile_type is not set.", @@ -254,12 +273,13 @@ def load( cfg_name = None cfg_schema = None cfg_schema_dir = None - + for cfg_layer in (self.project_user_config, self.project_config, self.global_user_config, self.global_config): if cfg_layer.profiles is None: try: - cfg_layer.init_from_file(validate_schema) + cfg_layer.init_from_file(validate_schema, suppress_config_file_warnings) except SecureProfileLoadFailed: + self.__logger.warning(f"Could not load secure properties for {cfg_layer.filepath}") warnings.warn( f"Could not load secure properties for {cfg_layer.filepath}", SecurePropsNotFoundWarning, @@ -314,6 +334,7 @@ def load( missing_props.add(item) if len(missing_props) > 0: + self.__logger.error(f"Failed to load secure values: {missing_props}") raise SecureValuesNotFound(values=missing_props) warnings.resetwarnings() @@ -366,6 +387,7 @@ def get_highest_priority_layer(self, json_path: str) -> Optional[ConfigFile]: highest_layer = layer if highest_layer is None: + self.__logger.error(f"Could not find a valid layer for {json_path}") raise FileNotFoundError(f"Could not find a valid layer for {json_path}") return highest_layer diff --git a/src/core/zowe/core_for_zowe_sdk/request_handler.py b/src/core/zowe/core_for_zowe_sdk/request_handler.py index 062bfd02..20791ceb 100644 --- a/src/core/zowe/core_for_zowe_sdk/request_handler.py +++ b/src/core/zowe/core_for_zowe_sdk/request_handler.py @@ -12,8 +12,10 @@ import requests import urllib3 +import logging from .exceptions import InvalidRequestMethod, RequestFailed, UnexpectedStatus +from .logger import Log class RequestHandler: @@ -28,7 +30,7 @@ class RequestHandler: List of supported request methods """ - def __init__(self, session_arguments): + def __init__(self, session_arguments, logger_name = __name__): """ Construct a RequestHandler object. @@ -36,17 +38,21 @@ def __init__(self, session_arguments): ---------- session_arguments The Zowe SDK session arguments + + logger_name + The logger name of the modules calling request handler """ self.session_arguments = session_arguments self.valid_methods = ["GET", "POST", "PUT", "DELETE"] self.__handle_ssl_warnings() + self.__logger = Log.registerLogger(__name__) def __handle_ssl_warnings(self): """Turn off warnings if the SSL verification argument if off.""" if not self.session_arguments["verify"]: urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - def perform_request(self, method, request_arguments, expected_code=[200]): + def perform_request(self, method, request_arguments, expected_code=[200], stream = False): """Execute an HTTP/HTTPS requests from given arguments and return validated response (JSON). Parameters @@ -57,6 +63,8 @@ def perform_request(self, method, request_arguments, expected_code=[200]): The dictionary containing the required arguments for the execution of the request expected_code: int The list containing the acceptable response codes (default is [200]) + stream: boolean + The boolean value whether the request is stream Returns ------- @@ -66,35 +74,14 @@ def perform_request(self, method, request_arguments, expected_code=[200]): self.method = method self.request_arguments = request_arguments self.expected_code = expected_code + self.__logger.debug(f"Request method: {self.method}, Request arguments: {self.request_arguments}, Expected code: {expected_code}") self.__validate_method() - self.__send_request() + self.__send_request(stream = stream) self.__validate_response() + if stream: + return self.response return self.__normalize_response() - def perform_streamed_request(self, method, request_arguments, expected_code=[200]): - """Execute a streamed HTTP/HTTPS requests from given arguments and return a raw response. - - Parameters - ---------- - method: str - The request method that should be used - request_arguments: dict - The dictionary containing the required arguments for the execution of the request - expected_code: int - The list containing the acceptable response codes (default is [200]) - - Returns - ------- - A raw response data - """ - self.method = method - self.request_arguments = request_arguments - self.expected_code = expected_code - self.__validate_method() - self.__send_request(stream=True) - self.__validate_response() - return self.response - def __validate_method(self): """Check if the input request method for the request is supported. @@ -104,6 +91,7 @@ def __validate_method(self): If the input request method is not supported """ if self.method not in self.valid_methods: + self.__logger.error(f"Invalid HTTP method input {self.method}") raise InvalidRequestMethod(self.method) def __send_request(self, stream=False): @@ -124,14 +112,16 @@ def __validate_response(self): If the HTTP/HTTPS request fails """ # Automatically checks if status code is between 200 and 400 - if self.response: + if self.response.ok: if self.response.status_code not in self.expected_code: + self.__logger.error(f"The status code from z/OSMF was: {self.expected_code}\nExpected: {self.response.status_code}\nRequest output:{self.response.text}") raise UnexpectedStatus(self.expected_code, self.response.status_code, self.response.text) else: output_str = str(self.response.request.url) output_str += "\n" + str(self.response.request.headers) output_str += "\n" + str(self.response.request.body) output_str += "\n" + str(self.response.text) + self.__logger.error(f"HTTP Request has failed with status code {self.response.status_code}. \n {output_str}") raise RequestFailed(self.response.status_code, output_str) def __normalize_response(self): diff --git a/src/core/zowe/core_for_zowe_sdk/sdk_api.py b/src/core/zowe/core_for_zowe_sdk/sdk_api.py index 146f29aa..dae4fc29 100644 --- a/src/core/zowe/core_for_zowe_sdk/sdk_api.py +++ b/src/core/zowe/core_for_zowe_sdk/sdk_api.py @@ -11,11 +11,13 @@ """ import urllib +import logging from . import session_constants from .exceptions import UnsupportedAuthType from .request_handler import RequestHandler from .session import ISession, Session +from .logger import Log class SdkApi: @@ -23,11 +25,13 @@ class SdkApi: Abstract class used to represent the base SDK API. """ - def __init__(self, profile, default_url): + def __init__(self, profile, default_url, logger_name = __name__): self.profile = profile session = Session(profile) self.session: ISession = session.load() + self.logger = Log.registerLogger(logger_name) + self.default_service_url = default_url self.default_headers = { "Content-Type": "application/json", @@ -44,7 +48,7 @@ def __init__(self, profile, default_url): "verify": self.session.rejectUnauthorized, "timeout": 30, } - self.request_handler = RequestHandler(self.session_arguments) + self.request_handler = RequestHandler(self.session_arguments, logger_name = logger_name) if self.session.type == session_constants.AUTH_TYPE_BASIC: self.request_arguments["auth"] = (self.session.user, self.session.password) @@ -52,8 +56,6 @@ def __init__(self, profile, default_url): self.default_headers["Authorization"] = f"Bearer {self.session.tokenValue}" elif self.session.type == session_constants.AUTH_TYPE_TOKEN: self.default_headers["Cookie"] = f"{self.session.tokenType}={self.session.tokenValue}" - else: - raise UnsupportedAuthType(self.session.type) def _create_custom_request_arguments(self): """Create a copy of the default request arguments dictionary. diff --git a/src/core/zowe/core_for_zowe_sdk/session.py b/src/core/zowe/core_for_zowe_sdk/session.py index 54647f38..fa2861bc 100644 --- a/src/core/zowe/core_for_zowe_sdk/session.py +++ b/src/core/zowe/core_for_zowe_sdk/session.py @@ -14,6 +14,7 @@ from typing import Optional from . import session_constants +from .logger import Log @dataclass @@ -42,9 +43,12 @@ class Session: def __init__(self, props: dict) -> None: # set host and port + self.__logger = Log.registerLogger(__name__) + if props.get("host") is not None: self.session: ISession = ISession(host=props.get("host")) else: + self.__logger.error("Host not supplied") raise Exception("Host must be supplied") # determine authentication type @@ -61,6 +65,7 @@ def __init__(self, props: dict) -> None: self.session.tokenValue = props.get("tokenValue") self.session.type = session_constants.AUTH_TYPE_BEARER else: + self.__logger.error("Authentication method not supplied") raise Exception("An authentication method must be supplied") # set additional parameters diff --git a/src/core/zowe/core_for_zowe_sdk/zosmf_profile.py b/src/core/zowe/core_for_zowe_sdk/zosmf_profile.py index 5ab3ea41..d9aa1893 100644 --- a/src/core/zowe/core_for_zowe_sdk/zosmf_profile.py +++ b/src/core/zowe/core_for_zowe_sdk/zosmf_profile.py @@ -19,6 +19,7 @@ from .connection import ApiConnection from .constants import constants from .exceptions import SecureProfileLoadFailed +from .logger import Log HAS_KEYRING = True try: @@ -53,6 +54,7 @@ def __init__(self, profile_name): The name of the Zowe z/OSMF profile """ self.profile_name = profile_name + self.__logger = Log.registerLogger(__name__) @property def profiles_dir(self): @@ -107,12 +109,14 @@ def __get_secure_value(self, name): def __load_secure_credentials(self): """Load secure credentials for a z/OSMF profile.""" if not HAS_KEYRING: + self.__logger.error(f"{self.profile_name} keyring module not installed") raise SecureProfileLoadFailed(self.profile_name, "Keyring module not installed") try: zosmf_user = self.__get_secure_value("user") zosmf_password = self.__get_secure_value("password") except Exception as e: + self.__logger.error(f"Failed to load secure profile '{self.profile_name}' because '{e}'") raise SecureProfileLoadFailed(self.profile_name, e) else: return (zosmf_user, zosmf_password) diff --git a/src/zos_console/zowe/zos_console_for_zowe_sdk/console.py b/src/zos_console/zowe/zos_console_for_zowe_sdk/console.py index 59817c86..42e5a3d8 100644 --- a/src/zos_console/zowe/zos_console_for_zowe_sdk/console.py +++ b/src/zos_console/zowe/zos_console_for_zowe_sdk/console.py @@ -22,7 +22,7 @@ def __init__(self, connection): connection The connection object """ - super().__init__(connection, "/zosmf/restconsoles/consoles/defcn") + super().__init__(connection, "/zosmf/restconsoles/consoles/defcn", logger_name=__name__) def issue_command(self, command, console=None): """Issues a command on z/OS Console. diff --git a/src/zos_files/zowe/zos_files_for_zowe_sdk/files.py b/src/zos_files/zowe/zos_files_for_zowe_sdk/files.py index efe9d539..688c24e8 100644 --- a/src/zos_files/zowe/zos_files_for_zowe_sdk/files.py +++ b/src/zos_files/zowe/zos_files_for_zowe_sdk/files.py @@ -10,7 +10,6 @@ Copyright Contributors to the Zowe Project. """ - import os from zowe.core_for_zowe_sdk import SdkApi @@ -44,7 +43,7 @@ def __init__(self, connection): Also update header to accept gzip encoded responses """ - super().__init__(connection, "/zosmf/restfiles/") + super().__init__(connection, "/zosmf/restfiles/", logger_name=__name__) self.default_headers["Accept-Encoding"] = "gzip" def list_files(self, path): @@ -232,6 +231,7 @@ def copy_dataset_or_member( if enq in ("SHR", "SHRW", "EXCLU"): data["enq"] = enq else: + self.logger.error("Invalid value for enq.") raise ValueError("Invalid value for enq.") if volser: data["from-dataset"]["volser"] = volser @@ -270,6 +270,7 @@ def create_data_set(self, dataset_name, options={}): if options.get("like") is None: if options.get("primary") is None or options.get("lrecl") is None: + self.logger.error("If 'like' is not specified, you must specify 'primary' or 'lrecl'.") raise ValueError("If 'like' is not specified, you must specify 'primary' or 'lrecl'.") for opt in ( @@ -292,44 +293,51 @@ def create_data_set(self, dataset_name, options={}): ): if opt == "dsorg": if options.get(opt) is not None and options[opt] not in ("PO", "PS"): + self.logger.error(f"{opt} is not 'PO' or 'PS'.") raise KeyError - if opt == "alcunit": + elif opt == "alcunit": if options.get(opt) is None: options[opt] = "TRK" else: if options[opt] not in ("CYL", "TRK"): + self.logger.error(f"{opt} is not 'CYL' or 'TRK'.") raise KeyError - if opt == "primary": + elif opt == "primary": if options.get(opt) is not None: if options["primary"] > 16777215: + self.logger.error("Specified value exceeds limit.") raise ValueError - if opt == "secondary": + elif opt == "secondary": if options.get("primary") is not None: if options.get(opt) is None: options["secondary"] = int(options["primary"] / 10) if options["secondary"] > 16777215: + self.logger.error("Specified value exceeds limit.") raise ValueError - if opt == "dirblk": + elif opt == "dirblk": if options.get(opt) is not None: if options.get("dsorg") == "PS": if options["dirblk"] != 0: + self.logger.error("Can't allocate directory blocks for files.") raise ValueError elif options.get("dsorg") == "PO": if options["dirblk"] == 0: + self.logger.error("Can't allocate empty directory blocks.") raise ValueError - if opt == "recfm": + elif opt == "recfm": if options.get(opt) is None: options[opt] = "F" else: if options[opt] not in ("F", "FB", "V", "VB", "U", "FBA", "FBM", "VBA", "VBM"): + self.logger.error("Invalid record format.") raise KeyError - if opt == "blksize": + elif opt == "blksize": if options.get(opt) is None and options.get("lrecl") is not None: options[opt] = options["lrecl"] @@ -356,6 +364,7 @@ def create_default_data_set(self, dataset_name: str, default_type: str): """ if default_type not in ("partitioned", "sequential", "classic", "c", "binary"): + self.logger.error("Invalid type for default data set.") raise ValueError("Invalid type for default data set.") custom_args = self._create_custom_request_arguments() @@ -443,7 +452,7 @@ def get_dsn_content_streamed(self, dataset_name): """ custom_args = self._create_custom_request_arguments() custom_args["url"] = "{}ds/{}".format(self.request_endpoint, self._encode_uri_component(dataset_name)) - response = self.request_handler.perform_streamed_request("GET", custom_args) + response = self.request_handler.perform_request("GET", custom_args, stream = True) return response def get_dsn_binary_content(self, dataset_name, with_prefixes=False): @@ -491,7 +500,7 @@ def get_dsn_binary_content_streamed(self, dataset_name, with_prefixes=False): custom_args["headers"]["X-IBM-Data-Type"] = "record" else: custom_args["headers"]["X-IBM-Data-Type"] = "binary" - response = self.request_handler.perform_streamed_request("GET", custom_args) + response = self.request_handler.perform_request("GET", custom_args, stream = True) return response def write_to_dsn(self, dataset_name, data, encoding=_ZOWE_FILES_DEFAULT_ENCODING): @@ -537,6 +546,7 @@ def upload_file_to_dsn(self, input_file, dataset_name, encoding=_ZOWE_FILES_DEFA with open(input_file, "rb") as in_file: response_json = self.write_to_dsn(dataset_name, in_file) else: + self.logger.error(f"File {input_file} not found.") raise FileNotFound(input_file) def write_to_uss(self, filepath_name, data, encoding=_ZOWE_FILES_DEFAULT_ENCODING): @@ -559,6 +569,7 @@ def upload_file_to_uss(self, input_file, filepath_name, encoding=_ZOWE_FILES_DEF with open(input_file, "r", encoding="utf-8") as in_file: response_json = self.write_to_uss(filepath_name, in_file) else: + self.logger.error(f"File {input_file} not found.") raise FileNotFound(input_file) def get_file_content_streamed(self, file_path, binary=False): @@ -573,7 +584,7 @@ def get_file_content_streamed(self, file_path, binary=False): custom_args["url"] = "{}fs/{}".format(self.request_endpoint, self._encode_uri_component(file_path.lstrip("/"))) if binary: custom_args["headers"]["X-IBM-Data-Type"] = "binary" - response = self.request_handler.perform_streamed_request("GET", custom_args) + response = self.request_handler.perform_request("GET", custom_args, stream=True) return response def download_uss(self, file_path, output_file, binary=False): @@ -610,10 +621,12 @@ def create_zFS_file_system(self, file_system_name, options={}): for key, value in options.items(): if key == "perms": if value < 0 or value > 777: + self.logger.error("Invalid Permissions Option.") raise exceptions.InvalidPermsOption(value) if key == "cylsPri" or key == "cylsSec": if value > constants.zos_file_constants["MaxAllocationQuantity"]: + self.logger.error("Maximum allocation quantity exceeded.") raise exceptions.MaxAllocationQuantityExceeded custom_args = self._create_custom_request_arguments() @@ -843,6 +856,7 @@ def rename_dataset_member(self, dataset_name: str, before_member_name: str, afte if enq in ("SHRW", "EXCLU"): data["enq"] = enq.strip() else: + self.logger.error("Invalid value for enq.") raise ValueError("Invalid value for enq.") custom_args = self._create_custom_request_arguments() diff --git a/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/jobs.py b/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/jobs.py index b9e13fca..bcceb8d1 100644 --- a/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/jobs.py +++ b/src/zos_jobs/zowe/zos_jobs_for_zowe_sdk/jobs.py @@ -9,6 +9,7 @@ Copyright Contributors to the Zowe Project. """ + import os from zowe.core_for_zowe_sdk import SdkApi @@ -33,7 +34,7 @@ def __init__(self, connection): connection The connection object """ - super().__init__(connection, "/zosmf/restjobs/jobs/") + super().__init__(connection, "/zosmf/restjobs/jobs/", logger_name=__name__) def get_job_status(self, jobname, jobid): """Retrieve the status of a given job on JES. @@ -75,6 +76,7 @@ def cancel_job(self, jobname: str, jobid: str, modify_version="2.0"): A JSON object containing the result of the request execution """ if modify_version not in ("1.0", "2.0"): + self.logger.error('Modify version not accepted; Must be "1.0" or "2.0"') raise ValueError('Accepted values for modify_version: "1.0" or "2.0"') custom_args = self._create_custom_request_arguments() @@ -104,6 +106,7 @@ def delete_job(self, jobname, jobid, modify_version="2.0"): A JSON object containing the result of the request execution """ if modify_version not in ("1.0", "2.0"): + self.logger.error('Modify version not accepted; Must be "1.0" or "2.0"') raise ValueError('Accepted values for modify_version: "1.0" or "2.0"') custom_args = self._create_custom_request_arguments() @@ -145,6 +148,7 @@ def change_job_class(self, jobname: str, jobid: str, class_name: str, modify_ver A JSON object containing the result of the request execution """ if modify_version not in ("1.0", "2.0"): + self.logger.error('Accepted values for modify_version: "1.0" or "2.0"') raise ValueError('Accepted values for modify_version: "1.0" or "2.0"') response_json = self._issue_job_request({"class": class_name}, jobname, jobid, modify_version) @@ -168,6 +172,7 @@ def hold_job(self, jobname: str, jobid: str, modify_version="2.0"): A JSON object containing the result of the request execution """ if modify_version not in ("1.0", "2.0"): + self.logger.error('Accepted values for modify_version: "1.0" or "2.0"') raise ValueError('Accepted values for modify_version: "1.0" or "2.0"') response_json = self._issue_job_request({"request": "hold"}, jobname, jobid, modify_version) @@ -191,6 +196,7 @@ def release_job(self, jobname: str, jobid: str, modify_version="2.0"): A JSON object containing the result of the request execution """ if modify_version not in ("1.0", "2.0"): + self.logger.error('Modify version not accepted; Must be "1.0" or "2.0"') raise ValueError('Accepted values for modify_version: "1.0" or "2.0"') response_json = self._issue_job_request({"request": "release"}, jobname, jobid, modify_version) @@ -271,6 +277,7 @@ def submit_from_local_file(self, jcl_path): file_content = jcl_file.read() return self.submit_plaintext(file_content) else: + self.logger.error("Provided argument is not a file path {}".format(jcl_path)) raise FileNotFoundError("Provided argument is not a file path {}".format(jcl_path)) def submit_plaintext(self, jcl): diff --git a/src/zos_tso/zowe/zos_tso_for_zowe_sdk/tso.py b/src/zos_tso/zowe/zos_tso_for_zowe_sdk/tso.py index 2a882f3f..53e273c8 100644 --- a/src/zos_tso/zowe/zos_tso_for_zowe_sdk/tso.py +++ b/src/zos_tso/zowe/zos_tso_for_zowe_sdk/tso.py @@ -38,7 +38,7 @@ def __init__(self, connection, tso_profile=None): connection The connection object """ - super().__init__(connection, "/zosmf/tsoApp/tso") + super().__init__(connection, "/zosmf/tsoApp/tso", logger_name=__name__) self.session_not_found = constants["TsoSessionNotFound"] self.tso_profile = tso_profile or {} diff --git a/src/zosmf/zowe/zosmf_for_zowe_sdk/zosmf.py b/src/zosmf/zowe/zosmf_for_zowe_sdk/zosmf.py index 22e4fa65..577e3fd3 100644 --- a/src/zosmf/zowe/zosmf_for_zowe_sdk/zosmf.py +++ b/src/zosmf/zowe/zosmf_for_zowe_sdk/zosmf.py @@ -31,7 +31,7 @@ def __init__(self, connection): connection The z/OSMF connection object (generated by the ZoweSDK object) """ - super().__init__(connection, "/zosmf/info") + super().__init__(connection, "/zosmf/info", logger_name=__name__) def get_info(self): """Return a JSON response from the GET request to z/OSMF info endpoint. diff --git a/tests/integration/fixtures/sample.jcl.tmp b/tests/integration/fixtures/sample.jcl.tmp new file mode 100644 index 00000000..39020987 --- /dev/null +++ b/tests/integration/fixtures/sample.jcl.tmp @@ -0,0 +1,5 @@ +//IEFBR14T JOB (AUTOMATION),CLASS=A,MSGCLASS=0, + +// MSGLEVEL=(1,1),REGION=0M,NOTIFY=&SYSUID + +//STEP1 EXEC PGM=IEFBR14 diff --git a/tests/unit/test_zowe_core.py b/tests/unit/test_zowe_core.py index 7b1d1188..459a52ad 100644 --- a/tests/unit/test_zowe_core.py +++ b/tests/unit/test_zowe_core.py @@ -8,6 +8,7 @@ import shutil import unittest from unittest import mock +import logging import commentjson from jsonschema import SchemaError, ValidationError, validate @@ -24,6 +25,7 @@ custom_warnings, exceptions, session_constants, + logger ) from zowe.core_for_zowe_sdk.validators import validate_config_json from zowe.secrets_for_zowe_sdk import keyring @@ -93,6 +95,24 @@ def test_object_should_be_instance_of_class(self): sdk_api = SdkApi(self.basic_props, self.default_url) self.assertIsInstance(sdk_api, SdkApi) + @mock.patch("logging.Logger.error") + def test_session_no_host_logger(self, mock_logger_error: mock.MagicMock): + props = {} + try: + sdk_api = SdkApi(props, self.default_url) + except Exception: + mock_logger_error.assert_called() + self.assertIn("Host", mock_logger_error.call_args[0][0]) + + @mock.patch("logging.Logger.error") + def test_session_no_authentication_logger(self, mock_logger_error: mock.MagicMock): + props = {"host": "test"} + try: + sdk_api = SdkApi(props, self.default_url) + except Exception: + mock_logger_error.assert_called() + self.assertIn("Authentication", mock_logger_error.call_args[0][0]) + def test_should_handle_basic_auth(self): """Created object should handle basic authentication.""" sdk_api = SdkApi(self.basic_props, self.default_url) @@ -150,16 +170,55 @@ def test_object_should_be_instance_of_class(self): request_handler = RequestHandler(self.session_arguments) self.assertIsInstance(request_handler, RequestHandler) + @mock.patch("logging.Logger.debug") + @mock.patch("logging.Logger.error") @mock.patch("requests.Session.send") - def test_perform_streamed_request(self, mock_send_request): + def test_perform_streamed_request(self, mock_send_request, mock_logger_error: mock.MagicMock, mock_logger_debug: mock.MagicMock): """Performing a streamed request should call 'send_request' method""" mock_send_request.return_value = mock.Mock(status_code=200) request_handler = RequestHandler(self.session_arguments) - request_handler.perform_streamed_request("GET", {"url": "https://www.zowe.org"}) + request_handler.perform_request("GET", {"url": "https://www.zowe.org"}, stream = True) + + mock_logger_error.assert_not_called() + mock_logger_debug.assert_called() + self.assertIn("Request method: GET", mock_logger_debug.call_args[0][0]) mock_send_request.assert_called_once() self.assertTrue(mock_send_request.call_args[1]["stream"]) + @mock.patch("logging.Logger.error") + def test_logger_unmatched_status_code(self, mock_logger_error: mock.MagicMock): + """Test logger with unexpeceted status code""" + request_handler = RequestHandler(self.session_arguments) + try: + request_handler.perform_request("GET", {"url": "https://www.zowe.org"}, expected_code= [0], stream = True) + except exceptions.UnexpectedStatus: + mock_logger_error.assert_called_once() + self.assertIn("The status code", mock_logger_error.call_args[0][0]) + + @mock.patch("logging.Logger.error") + def test_logger_perform_request_invalid_method(self, mock_logger_error: mock.MagicMock): + """Test logger with invalid request method""" + request_handler = RequestHandler(self.session_arguments) + try: + request_handler.perform_request("Invalid method", {"url": "https://www.zowe.org"}, stream = True) + except exceptions.InvalidRequestMethod: + mock_logger_error.assert_called_once() + self.assertIn("Invalid HTTP method input", mock_logger_error.call_args[0][0]) + + @mock.patch("logging.Logger.error") + @mock.patch("requests.Session.send") + def test_logger_invalid_status_code(self, mock_send_request, mock_logger_error: mock.MagicMock): + mock_send_request.return_value = mock.Mock(ok=False) + request_handler = RequestHandler(self.session_arguments) + try: + request_handler.perform_request("GET", {"url": "https://www.zowe.org"}, stream = True) + except exceptions.RequestFailed: + mock_logger_error.assert_called_once() + self.assertIn("HTTP Request has failed", mock_logger_error.call_args[0][0]) + mock_logger_error.assert_called_once + + class TestZosmfProfileClass(unittest.TestCase): """ZosmfProfile class unit tests.""" @@ -354,8 +413,9 @@ def test_profile_loading_with_user_overridden_properties(self, get_pass_func): } self.assertEqual(props, expected_props) + @mock.patch("logging.Logger.warning") @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) - def test_profile_loading_exception(self, get_pass_func): + def test_profile_loading_exception(self, get_pass_func, mock_logger_warning: mock.MagicMock): """ Test correct exceptions are being thrown when a profile is not found. @@ -369,8 +429,62 @@ def test_profile_loading_exception(self, get_pass_func): shutil.copy(self.original_file_path, cwd_up_file_path) # Test + self.setUpCreds(cwd_up_file_path, secure_props={}) config_file = ConfigFile(name=self.custom_appname, type="team_config") props: dict = config_file.get_profile(profile_name="non_existent_profile", validate_schema=False) + self.assertEqual(mock_logger_warning.call_args[0][0], "Profile non_existent_profile not found") + + @mock.patch("logging.Logger.error") + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) + def test_profile_empty_exception(self, get_pass_func, mock_logger_error: mock.MagicMock): + """ + Test correct exceptions are being thrown when a profile is + not found. + + Filename and Filetype will be set to None. + """ + with self.assertRaises(exceptions.ProfileNotFound): + # Setup + cwd_up_dir_path = os.path.dirname(CWD) + cwd_up_file_path = os.path.join(cwd_up_dir_path, f"{self.custom_appname}.config.json") + shutil.copy(self.original_file_path, cwd_up_file_path) + + # Test + self.setUpCreds(cwd_up_file_path, secure_props={}) + config_file = ConfigFile(name=self.custom_appname, type="team_config") + props: dict = config_file.get_profile(profile_name=None,profile_type=None,validate_schema=False) + self.assertEqual(mock_logger_error.call_args[0][0], "Failed to load profile: profile_name and profile_type were not provided.") + + @mock.patch("logging.Logger.error") + @mock.patch("logging.Logger.warning") + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) + def test_get_profilename_from_profiletype_invalid_profile_type(self, get_pass_func, mock_logger_warning: mock.MagicMock, mock_logger_error: mock.MagicMock): + """ + Test correct warnings and exceptions are being thrown with + empty default, invalid profile type. + + """ + with self.assertRaises(exceptions.ProfileNotFound): + config_file = ConfigFile(name="name", type="team_config", defaults={}, profiles={'a': {'none' : 'none'}}) + config_file.get_profilename_from_profiletype('test') + + mock_logger_warning.assert_any_call("Given profile type 'test' has no default profile name") + mock_logger_warning.assert_any_call("Profile 'a' has no type attribute") + mock_logger_error.assert_called_once_with("No profile with matching profile_type 'test' found") + + + @mock.patch("logging.Logger.warning") + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) + def test_validate_schema_logger(self, get_pass_func, mock_logger_warning: mock.MagicMock): + """ + Test correct exceptions are being thrown when schema property is not set. + + Schema property will be initialized to None. + """ + with self.assertWarns(UserWarning): + config_file = ConfigFile(name="name", type="team_config") + config_file.validate_schema() + self.assertEqual(mock_logger_warning.call_args[0][0], "Could not find $schema property") @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password_exception) def test_secure_props_loading_warning(self, get_pass_func): @@ -591,6 +705,7 @@ def test_profile_loading_with_valid_schema(self, get_pass_func): prof_manager.config_dir = self.custom_dir props: dict = prof_manager.load(profile_name="zosmf") + @mock.patch("zowe.secrets_for_zowe_sdk.keyring.get_password", side_effect=keyring_get_password) def test_profile_loading_with_invalid_schema(self, get_pass_func): """ @@ -616,7 +731,7 @@ def test_profile_loading_with_invalid_schema(self, get_pass_func): "profiles.zosmf.properties.password": "password", }, ) - + # Test prof_manager = ProfileManager(appname="invalid.zowe") prof_manager.config_dir = self.custom_dir @@ -949,3 +1064,14 @@ def test_validate_config_json_invalid(self): validate_config_json(path_to_invalid_config, path_to_invalid_schema, cwd=FIXTURES_PATH) self.assertEqual(str(actual_info.exception), str(expected_info.exception)) + + +class test_logger_setLoggerLevel(TestCase): + + def test_logger_setLoggerLevel(self): + """Test setLoggerLevel""" + profile = ProfileManager() + test_logger = logging.getLogger("zowe.core_for_zowe_sdk.profile_manager") + test_value = logging.DEBUG + logger.Log.setLoggerLevel(test_value) + self.assertEqual(test_logger.level, test_value) \ No newline at end of file