diff --git a/dmtf/redfish-setup.sh b/dmtf/redfish-setup.sh index 967be07..63a1786 100644 --- a/dmtf/redfish-setup.sh +++ b/dmtf/redfish-setup.sh @@ -1,6 +1,7 @@ #!/bin/bash function start_apache { + [ -f "/run/apache2/apache2.pid" ] && rm "/run/apache2/apache2.pid" echo "Launching apache2 in foreground with /usr/sbin/apache2ctl -DFOREGROUND -k start" /usr/sbin/apache2ctl -DFOREGROUND -k start } diff --git a/examples/simple-proliant.py b/examples/simple-proliant.py index a8f5174..468527b 100644 --- a/examples/simple-proliant.py +++ b/examples/simple-proliant.py @@ -6,7 +6,7 @@ import os import sys import json import redfish -from time import sleep + # Get $HOME environment. HOME = os.getenv('HOME') @@ -24,17 +24,26 @@ except IOError as e: print(e) sys.exit(1) -URL = config["Nodes"]["default"]["url"] -USER_NAME = config["Nodes"]["default"]["login"] -PASSWORD = config["Nodes"]["default"]["password"] +URL = config["Managers"]["default"]["url"] +USER_NAME = config["Managers"]["default"]["login"] +PASSWORD = config["Managers"]["default"]["password"] ''' remote_mgmt is a redfish.RedfishConnection object ''' -remote_mgmt = redfish.connect(URL, USER_NAME, PASSWORD, verify_cert=False) +try: + remote_mgmt = redfish.connect(URL, + USER_NAME, + PASSWORD, + simulator=False, + verify_cert=False) +except redfish.exception.RedfishException as e: + sys.stderr.write(str(e.message)) + sys.stderr.write(str(e.advices)) + sys.exit(1) print ("Redfish API version : %s \n" % remote_mgmt.get_api_version()) # Uncomment following line to reset the blade !!! -#remote_mgmt.Systems.systems_list[0].reset_system() +# remote_mgmt.Systems.systems_list[0].reset_system() # TODO : create an attribute to link the managed system directly # and avoid systems_list[0] diff --git a/examples/simple-simulator.py b/examples/simple-simulator.py index 52d9f18..b9db1af 100644 --- a/examples/simple-simulator.py +++ b/examples/simple-simulator.py @@ -23,13 +23,22 @@ except IOError as e: print(e) sys.exit(1) -URL = config["Nodes"]["default"]["url"] -USER_NAME = config["Nodes"]["default"]["login"] -PASSWORD = config["Nodes"]["default"]["password"] +URL = config["Managers"]["default"]["url"] +USER_NAME = config["Managers"]["default"]["login"] +PASSWORD = config["Managers"]["default"]["password"] ''' remoteMgmt is a redfish.RedfishConnection object ''' -remote_mgmt = redfish.connect(URL, USER_NAME, PASSWORD, - simulator=True, enforceSSL=False) +try: + remote_mgmt = redfish.connect(URL, + USER_NAME, + PASSWORD, + simulator=True, + enforceSSL=False) +except redfish.exception.RedfishException as e: + sys.stderr.write(e.message) + sys.stderr.write(e.advices) + sys.exit(1) + print("Redfish API version : {} \n".format(remote_mgmt.get_api_version())) print("UUID : {} \n".format(remote_mgmt.Root.get_api_UUID())) diff --git a/redfish-client/redfish-client.py b/redfish-client/redfish-client.py index c1be6b6..5d011c1 100755 --- a/redfish-client/redfish-client.py +++ b/redfish-client/redfish-client.py @@ -2,125 +2,391 @@ # coding=utf-8 -""" +''' redfish-client Usage: redfish-client.py [options] config add <manager_name> <manager_url> [<login>] [<password>] redfish-client.py [options] config del <manager_name> - redfish-client.py [options] config modify <manager_name> (url | login | password) <changed_value> + redfish-client.py [options] config modify <manager_name> (manager_name | url | login | password) <changed_value> redfish-client.py [options] config show redfish-client.py [options] config showall + redfish-client.py [options] manager getinfo [<manager_name>] redfish-client.py (-h | --help) redfish-client.py --version - + Options: - -h --help Show this screen. - --version Show version. - --conf_file FILE Configuration file [default: ~/.redfish.conf]. + -h --help Show this screen. + --version Show version. + --conf_file FILE Configuration file [default: ~/.redfish.conf] + --insecure Check SSL certificats + --debug LEVEL Run in debug mode, LEVEL from 1 to 3 increase verbosity + Security warning LEVEL > 1 could reveal password into the logs + --debugfile FILE Specify the client debugfile [default: redfish-client.log] + --libdebugfile FILE Specify python-redfish library log file [default: /var/log/python-redfish/python-redfish.log] - -config commands manage the configuration file. - -""" +config commands : manage the configuration file. +manager commands : manage the manager (Ligh out management). If <manager_name> + is not provided use the 'default' entry +''' import os import sys import json import pprint import docopt +import logging +import redfish +import requests.packages.urllib3 +import jinja2 class ConfigFile(object): + '''redfisht-client configuration file management''' def __init__(self, config_file): - self._config_file = config_file + '''Initialize the configuration file + + Open and load configuration file data. + If the file does not exist create an empty one ready to receive data + + :param config_file: File name of the configuration file + default: ~/.redfish.conf + :type str + :returns: Nothing + + ''' + self._config_file = config_file # read json file try: with open(self._config_file) as json_data: self.data = json.load(json_data) json_data.close() except (ValueError, IOError): - self.data = {"Managers":{}} + self.data = {'Managers': {}} def save(self): + '''Save the configuration file data''' try: - with open(self._config_file , 'w') as json_data: + with open(self._config_file, 'w') as json_data: json.dump(self.data, json_data) json_data.close() except IOError as e: - print(e.msg) - sys.exit(1) - + print(e.msg) + sys.exit(1) + + def manager_incorect(self, exception): + ''' Log and exit if manager name is incorect''' + logger.error('Incorect manager name : %s' % exception.args) + sys.exit(1) + + def check_manager(self, manager_name): + '''Check if the manager exists in configuration file + + :param manager_name: Name of the manager + :type str + + ''' + try: + if manager_name not in self.get_managers(): + raise KeyError(manager_name) + except KeyError as e: + self.manager_incorect(e) + def add_manager(self, manager_name, url, login, password): + '''Add a manager to the configuration file + + :param manager_name: Name of the manager + :type str + :param url: Url of the manager + :type str + :param login: Login of the manager + :type str + :param password: Password of the manager + :type str + + ''' + self.data['Managers'][manager_name] = {} self.data['Managers'][manager_name]['url'] = url - if login != None: - self.data['Managers'][manager_name]['login'] = login - if password != None: - self.data['Managers'][manager_name]['password'] = password - + if login is None: + login = '' + if password is None: + password = '' + self.data['Managers'][manager_name]['login'] = login + self.data['Managers'][manager_name]['password'] = password + + def modify_manager(self, manager_name, parameter, parameter_value): + '''Modify the manager settings + + :param manager name: Name of the manager + :type str + :param parameter: url | login | password + :type str + :param parameter_value: Value of the parameter + :type str + :returns: Nothing + + ''' + + if parameter == 'url': + try: + self.data['Managers'][manager_name]['url'] = parameter_value + except KeyError as e: + self.manager_incorect(e) + elif parameter == 'login': + try: + self.data['Managers'][manager_name]['login'] = parameter_value + except KeyError as e: + self.manager_incorect(e) + elif parameter == 'password': + try: + self.data['Managers'][manager_name]['password'] = parameter_value + except KeyError as e: + self.manager_incorect(e) + elif parameter == 'manager_name': + # Create a new entry with the new name + self.add_manager(parameter_value, + self.data['Managers'][manager_name]['url'], + self.data['Managers'][manager_name]['login'], + self.data['Managers'][manager_name]['password'], + ) + # Remove the previous one + self.delete_manager(manager_name) + + def delete_manager(self, manager_name): + '''Delete manager + + :param manager name: Name of the manager + :type str + :returns: Nothing + + ''' + + try: + del self.data['Managers'][manager_name] + except KeyError as e: + self.manager_incorect(e) + def get_managers(self): + '''Get manager configured + + :returns: Managers + :type list + + ''' managers = [] for manager in self.data['Managers']: - managers += [manager] + managers += [manager] return(managers) - + def get_manager_info(self, manager): + '''Show manager info (url, login, password) + + :param manager: Name of the manager + :type str + :returns: info containing url, login, password + :type dict + + ''' info = {} - url=self.data['Managers'][manager]['url'] - login=self.data['Managers'][manager]['login'] - password=self.data['Managers'][manager]['password'] - info={'url':url, 'login':login, 'password':password} - return(info) + url = self.data['Managers'][manager]['url'] + login = self.data['Managers'][manager]['login'] + password = self.data['Managers'][manager]['password'] + info = {'url': url, 'login': login, 'password': password} + return(info) + class RedfishClientException(Exception): - """Base class for redfish client exceptions""" + + '''Base class for redfish client exceptions''' + def __init__(self, message=None, **kwargs): self.kwargs = kwargs - self.message = message + self.message = message if __name__ == '__main__': + '''Main application redfish-client''' # Functions + def show_manager(all=False): - print("Managers configured :") + '''Display manager info + + :param all: Add login and password info + :type bool + :returns: Nothing + + ''' + print('Managers configured :') for manager in conf_file.get_managers(): print(manager) - if all == True: + if all is True: info = conf_file.get_manager_info(manager) - print("\tUrl : {}".format(info['url'])) - print("\tLogin : {}".format(info['login'])) - print("\tPassword : {}".format(info['password'])) - + print('\tUrl : {}'.format(info['url'])) + print('\tLogin : {}'.format(info['login'])) + print('\tPassword : {}'.format(info['password'])) + + def get_manager_info(manager_name, check_SSL): + connection_parameters = conf_file.get_manager_info(manager_name) + if not connection_parameters['login']: + simulator = True + enforceSSL = False + else: + simulator = False + enforceSSL = True + try: + print('Gathering data from manager, please wait...\n') + # TODO : Add a rotating star showing program is running ? + # Could be a nice exercice for learning python. :) + logger.info('Gathering data from manager') + remote_mgmt = redfish.connect(connection_parameters['url'], + connection_parameters['login'], + connection_parameters['password'], + verify_cert=check_SSL, + simulator=simulator, + enforceSSL=enforceSSL + ) + except redfish.exception.RedfishException as e: + sys.stderr.write(str(e.message)) + sys.stderr.write(str(e.advices)) + sys.exit(1) + + # Display manager information using jinja2 template + template = jinja2_env.get_template("manager_info.template") + print template.render(r=remote_mgmt) + + + # Main program + redfishclient_version = "redfish-client 0.1" + + # Parse and manage arguments + arguments = docopt.docopt(__doc__, version=redfishclient_version) + + # Check debuging options + # Debugging LEVEL : + # 1- Only client + # 2- Client and lib + # 3- Client and lib + Tortilla + + loglevel = {"console_logger_level": "nolog", + "file_logger_level": logging.INFO, + "tortilla": False, + "lib_console_logger_level": "nolog", + "lib_file_logger_level": logging.INFO, + "urllib3_disable_warning": True} + + if arguments['--debug'] == '1': + loglevel['console_logger_level'] = logging.DEBUG + loglevel['file_logger_level'] = logging.DEBUG + elif arguments['--debug'] == '2': + loglevel['console_logger_level'] = logging.DEBUG + loglevel['file_logger_level'] = logging.DEBUG + loglevel['lib_console_logger_level'] = logging.DEBUG + loglevel['lib_file_logger_level'] = logging.DEBUG + loglevel['urllib3_disable_warning'] = False + elif arguments['--debug'] == '3': + loglevel['console_logger_level'] = logging.DEBUG + loglevel['file_logger_level'] = logging.DEBUG + loglevel['lib_console_logger_level'] = logging.DEBUG + loglevel['lib_file_logger_level'] = logging.DEBUG + loglevel['urllib3_disable_warning'] = False + loglevel['tortilla'] = True + + # Initialize logger according to command line parameters + logger = redfish.config.initialize_logger(arguments['--debugfile'], + loglevel['console_logger_level'], + loglevel['file_logger_level'], + __name__) + redfish.config.REDFISH_LOGFILE = arguments['--libdebugfile'] + redfish.config.TORTILLADEBUG = loglevel['tortilla'] + redfish.config.CONSOLE_LOGGER_LEVEL = loglevel['lib_console_logger_level'] + redfish.config.FILE_LOGGER_LEVEL = loglevel['lib_file_logger_level'] + # Avoid warning messages from request / urllib3 + # SecurityWarning: Certificate has no `subjectAltName`, falling back + # to check for a `commonName` for now. This feature is being removed + # by major browsers and deprecated by RFC 2818. + # (See https://github.com/shazow/urllib3/issues/497 for details.) + if loglevel['urllib3_disable_warning'] is True: + requests.packages.urllib3.disable_warnings() + + logger.info("*** Starting %s ***" % redfishclient_version) + logger.info("Arguments parsed") + logger.debug(arguments) + # Get $HOME environment. HOME = os.getenv('HOME') - if HOME == '': - print("$HOME environment variable not set, please check your system") + if not HOME: + print('$HOME environment variable not set, please check your system') + logger.error('$HOME environment variable not set') sys.exit(1) - - arguments = docopt.docopt(__doc__, version='redfish-client 0.1') - print(arguments) + logger.debug("Home directory : %s" % HOME) arguments['--conf_file'] = arguments['--conf_file'].replace('~', HOME) - conf_file = ConfigFile(arguments['--conf_file']) + + # Initialize Template system (jinja2) + # TODO : set the template file location into cmd line default to /usr/share/python-redfish/templates ? + logger.debug("Initialize template system") + jinja2_env = jinja2.Environment(loader=jinja2.FileSystemLoader("templates")) - - if arguments['config'] == True: - if arguments['show'] == True: + if arguments['config'] is True: + logger.debug("Config commands") + if arguments['show'] is True: + logger.debug('show command') show_manager() - elif arguments['showall'] == True: + elif arguments['showall'] is True: + logger.debug('showall command') show_manager(True) - elif arguments['add'] == True: + elif arguments['add'] is True: + logger.debug('add command') conf_file.add_manager(arguments['<manager_name>'], arguments['<manager_url>'], arguments['<login>'], - arguments['password']) - pprint.pprint(conf_file.data) - - conf_file.save() - + arguments['<password>']) + logger.debug(conf_file.data) + conf_file.save() + elif arguments['del'] is True: + logger.debug('del command') + conf_file.delete_manager(arguments['<manager_name>']) + logger.debug(conf_file.data) + conf_file.save() + elif arguments['modify'] is True: + logger.debug('modify command') + if arguments['url'] is not False: + conf_file.modify_manager(arguments['<manager_name>'], + 'url', + arguments['<changed_value>']) + elif arguments['login'] is not False: + conf_file.modify_manager(arguments['<manager_name>'], + 'login', + arguments['<changed_value>']) + elif arguments['password'] is not False: + conf_file.modify_manager(arguments['<manager_name>'], + 'password', + arguments['<changed_value>']) + elif arguments['manager_name'] is not False: + conf_file.modify_manager(arguments['<manager_name>'], + 'manager_name', + arguments['<changed_value>']) + logger.debug(conf_file.data) + conf_file.save() + if arguments['manager'] is True: + logger.debug("Manager commands") + if arguments['getinfo'] is True: + logger.debug('getinfo command') + # If manager is not defined set it to 'default' + if not arguments['<manager_name>']: + manager_name = 'default' + else: + manager_name = arguments['<manager_name>'] + # Check if the default section is available in our conf file + conf_file.check_manager(manager_name) + if arguments['--insecure'] is True: + get_manager_info(manager_name, False) + else: + get_manager_info(manager_name, True) + logger.info("Client session teminated") sys.exit(0) diff --git a/redfish-client/templates/bla.templates b/redfish-client/templates/bla.templates new file mode 100644 index 0000000..23b6b61 --- /dev/null +++ b/redfish-client/templates/bla.templates @@ -0,0 +1,32 @@ + #======================================================================= + # print('Redfish API version : %s' % remote_mgmt.get_api_version()) + # print(remote_mgmt.Root.get_name()) + # print('\n') + # print('Managers information :') + # print('----------------------') + # for manager_index in sorted(remote_mgmt.Managers.managers_dict): + # manager = remote_mgmt.Managers.managers_dict[manager_index] + # print('\nManager id {} :').format(manager_index) + # print('UUID : {}').format(manager.get_uuid()) + # print('Type : {}').format(manager.get_type()) + # print('Firmware version : {}').format(manager.get_firmware_version()) + # print('State : {}').format(manager.get_status()) + # print manager.get_managed_chassis() + # print manager.get_managed_systems() + # print('Ethernet interfaces :') + # try : + # for ethernetinterface_index in sorted(manager.ethernet_interfaces_collection.ethernet_interfaces_dict): + # ei = manager.ethernet_interfaces_collection.ethernet_interfaces_dict[ethernetinterface_index] + # print('\nEthernet Interface id {} :').format(ethernetinterface_index) + # print(ei.get_name()) + # print(ei.get_parameter('FQDN')) + # print ei.get_ipv4() + # print ei.get_ipv6() + # except AttributeError: + # # We don't have ethernet interfaces + # pass + #======================================================================= + + +Redfish API version : remote_mgmt.get_api_version() +remote_mgmt.Root.get_name() \ No newline at end of file diff --git a/redfish-client/templates/manager_info.template b/redfish-client/templates/manager_info.template new file mode 100644 index 0000000..71961ba --- /dev/null +++ b/redfish-client/templates/manager_info.template @@ -0,0 +1,31 @@ +Redfish API version : {{ r.get_api_version() }} +{{ r.Root.get_name() }} + +Managers information : +====================== +{% for manager_index in r.Managers.managers_dict | sort %} +{%- set manager = r.Managers.managers_dict[manager_index] %} +Manager id {{ manager_index }}: +UUID : {{ manager.get_uuid() }} +Type : {{ manager.get_type() }} +Firmware version : {{ manager.get_firmware_version() }} +State : {{ manager.get_status() }} +Ethernet Interface : +{%- if manager.ethernet_interfaces_collection %} +{%- for ethernetinterface_index in manager.ethernet_interfaces_collection.ethernet_interfaces_dict | sort %} +{%- set ei = manager.ethernet_interfaces_collection.ethernet_interfaces_dict[ethernetinterface_index] %} + Ethernet Interface id {{ ethernetinterface_index }} : + {{ ei.get_name() }} + FQDN : {{ ei.get_fqdn() }} + Address ipv4 : {{ ei.get_ipv4() | join(', ') }} + Address ipv6 : {{ ei.get_ipv6() | join(', ') }} +{%- endfor %} +{%- else %} + This manager has no ethernet interface +{%- endif %} +Managed Chassis : + {{ manager.get_managed_chassis() | join(', ') }} +Managed System : + {{ manager.get_managed_systems() | join(', ') }} +---------------------------- +{% endfor %} \ No newline at end of file diff --git a/redfish/config.py b/redfish/config.py index 6bd9341..0799aeb 100644 --- a/redfish/config.py +++ b/redfish/config.py @@ -4,34 +4,46 @@ import logging from logging.handlers import RotatingFileHandler # Global variable definition -TORTILLADEBUG = True + logger = None +TORTILLADEBUG = True +REDFISH_LOGFILE = "/var/log/python-redfish/python-redfish.log" +CONSOLE_LOGGER_LEVEL = logging.DEBUG +FILE_LOGGER_LEVEL = logging.DEBUG -def initialize_logger(redfish_logfile): - """Return api version. +def initialize_logger(REDFISH_LOGFILE, + CONSOLE_LOGGER_LEVEL, + FILE_LOGGER_LEVEL, + logger_name=None): + '''Initialize a global logger to track application behaviour - :param redfish_logfile: redfish log + :param redfish_logfile: Log filename :type str - :returns: True + :param screen_logger_level: Console log level + (logging.DEBUG, logging.ERROR, ..) or nolog + :type logging constant or string + :param file_logger_level: File log level + :type logging constant + :returns: logging object - """ - global logger - logger = logging.getLogger() - + ''' + + logger = logging.getLogger(logger_name) logger.setLevel(logging.DEBUG) formatter = logging.Formatter( '%(asctime)s :: %(levelname)s :: %(message)s' ) - file_handler = RotatingFileHandler(redfish_logfile, 'a', 1000000, 1) + file_handler = RotatingFileHandler(REDFISH_LOGFILE, 'a', 1000000, 1) # First logger to file - file_handler.setLevel(logging.DEBUG) + file_handler.setLevel(FILE_LOGGER_LEVEL) file_handler.setFormatter(formatter) logger.addHandler(file_handler) # Second logger to console - steam_handler = logging.StreamHandler() - steam_handler.setLevel(logging.DEBUG) - logger.addHandler(steam_handler) - return True \ No newline at end of file + if CONSOLE_LOGGER_LEVEL != "nolog": + steam_handler = logging.StreamHandler() + steam_handler.setLevel(CONSOLE_LOGGER_LEVEL) + logger.addHandler(steam_handler) + return logger diff --git a/redfish/exception.py b/redfish/exception.py index 6038480..5152f80 100644 --- a/redfish/exception.py +++ b/redfish/exception.py @@ -1,21 +1,52 @@ # -*- coding: utf-8 -*- -import sys + import config + class RedfishException(Exception): """Base class for redfish exceptions""" - def __init__(self, message=None, **kwargs): + def __init__(self, message, **kwargs): self.kwargs = kwargs self.message = message + self.advices = None + config.logger.error(message) + + +class ConnectionFailureException(RedfishException): + def __init__(self, message, **kwargs): + super(ConnectionFailureException, self).__init__(message, **kwargs) + self.advices = '1- Check if the url is the correct one\n' + \ + '2- Check if your device is answering on the network\n' + + +class InvalidRedfishContentException(RedfishException): + def __init__(self, message, **kwargs): + super(InvalidRedfishContentException, self).__init__(message, **kwargs) + self.advices = \ + '1- Check if the url is the correct one\n' + \ + ' Most of the time you are not pointing to the rest API\n' + + +class NonTrustedCertificatException(RedfishException): + def __init__(self, message, **kwargs): + super(NonTrustedCertificatException, self).__init__(message, **kwargs) + self.advices = \ + '1- Check if the url is the correct one\n' + \ + '2- Check if your device has a valid trusted certificat\n' + \ + ' You can use openssl to validate it using the command :\n' + \ + ' openssl s_client -showcerts -connect <server>:443\n' class AuthenticationFailureException(RedfishException): - def __init__(self, message=None, **kwargs): - super(AuthenticationFailureException, self).__init__(message=None, **kwargs) - config.logger.error(message) - # TODO - # Give a bit more details about the failure (check login etc...) - sys.exit(1) - + def __init__(self, message, **kwargs): + super(AuthenticationFailureException, self).__init__(message, **kwargs) + self.message += str(kwargs['code']) + self.queryAnswer = kwargs['queryAnswer'] + if kwargs['code'] == 400: + self.message += ': ' + self.queryAnswer['Messages'][0]['MessageID'] + self.advices = '1- Check your credentials\n' + self.message += '\n' + + class LogoutFailureException(RedfishException): pass diff --git a/redfish/main.py b/redfish/main.py index fbe9f21..3a28cf0 100644 --- a/redfish/main.py +++ b/redfish/main.py @@ -117,7 +117,7 @@ Clients should always be prepared for: # coding=utf-8 -import sys + import json from urlparse import urlparse import requests @@ -126,21 +126,7 @@ import types import mapping import exception -# Global variable definition -redfish_logfile = "/var/log/python-redfish/python-redfish.log" - -# =============================================================================== -# TODO : create method to set logging level and TORTILLADEBUG. -# =============================================================================== - - -def set_log_file(logfile): - global redfish_logfile - redfish_logfile = logfile - return True - - -""" Function to wrap RedfishConnection """ +"""Function to wrap RedfishConnection""" def connect( @@ -150,9 +136,8 @@ def connect( simulator=False, enforceSSL=True, verify_cert=True - ): - global redfish_logfile - config.initialize_logger(redfish_logfile) + ): + return RedfishConnection( url, user, @@ -173,9 +158,16 @@ class RedfishConnection(object): simulator=False, enforceSSL=True, verify_cert=True - ): + ): """Initialize a connection to a Redfish service.""" - super(RedfishConnection, self).__init__() + # Specify a name for the logger as recommended by the logging + # documentation. However for strange reason requests logs are not + # anymore capture in the log file. + # TODO : Check strange behavior about requests logs. + config.logger = config.initialize_logger(config.REDFISH_LOGFILE, + config.CONSOLE_LOGGER_LEVEL, + config.FILE_LOGGER_LEVEL, + __name__) config.logger.info("Initialize python-redfish") @@ -217,9 +209,10 @@ class RedfishConnection(object): config.logger.info("API Version : %s", self.get_api_version()) mapping.redfish_version = self.get_api_version() + mapping.redfish_root_name = self.Root.get_name() - # Instanciate a global mapping object to handle Redfish version variation - mapping.redfish_mapper = mapping.RedfishVersionMapping(self.get_api_version()) + # Instantiate a global mapping object to handle Redfish version variation + mapping.redfish_mapper = mapping.RedfishVersionMapping(self.get_api_version(), self.Root.get_name()) # Now we need to login otherwise we are not allowed to extract data if self.__simulator is False: @@ -233,7 +226,7 @@ class RedfishConnection(object): - # Struture change with mockup 1.0.0, there is no links + # Structure change with mockup 1.0.0, there is no links # section anymore. # =================================================================== # TODO : Add a switch to allow the both structure @@ -271,7 +264,7 @@ class RedfishConnection(object): # # print self.systemCollection.Name # - # ======================================================================== + # ======================================================================== def get_api_version(self): """Return api version. @@ -286,14 +279,15 @@ class RedfishConnection(object): url = self.Root.get_link_url( mapping.redfish_mapper.map_sessionservice() ) - - # Handle login with redfish 1.00, url must be : + + # Handle login with redfish 1.00, url must be : # /rest/v1/SessionService/Sessions as specified by the specification if float(mapping.redfish_version) >= 1.00: url += '/Sessions' # Craft request body and header requestBody = {"UserName": self.connection_parameters.user_name , "Password": self.connection_parameters.password} + config.logger.debug(requestBody) header = {'Content-type': 'application/json'} # ======================================================================= # Tortilla seems not able to provide the header of a post request answer. @@ -308,13 +302,16 @@ class RedfishConnection(object): headers=header, verify=self.connection_parameters.verify_cert ) - + # ======================================================================= # TODO : Manage exception with a class. # ======================================================================= if auth.status_code != 201: - raise exception.AuthenticationFailureException("Login request return an invalid status code") - #sysraise "Error getting token", auth.status_code + try: + answer=auth.json() + except ValueError as e: + answer = "" + raise exception.AuthenticationFailureException("Login request return an invalid status code ", code=auth.status_code, queryAnswer=answer) self.connection_parameters.auth_token = auth.headers.get("x-auth-token") self.connection_parameters.user_uri = auth.headers.get("location") diff --git a/redfish/mapping.py b/redfish/mapping.py index 309e23a..f999cd2 100644 --- a/redfish/mapping.py +++ b/redfish/mapping.py @@ -2,30 +2,51 @@ redfish_mapper = None redfish_version = None +redfish_root_name = None class RedfishVersionMapping(object): - """Implements basic url path mapping beetween Redfish versions.""" + '''Implements basic url path mapping beetween Redfish versions.''' - def __init__(self, version): + def __init__(self, version, rootname): self.__version = version + self.__rootname = rootname def map_sessionservice(self): - if self.__version == "0.95": - return "Sessions" - return("SessionService") - + if self.__version == '0.95': + return 'Sessions' + return 'SessionService' - def map_links(self): - if self.__version == "0.95": - return "links" - return("Links") + def map_links(self, data_dict=None): + if data_dict == None: + if self.__version == '0.95': + return 'links' + else: + # Checking if we have Links or links. + # This is to deal with proliant firmware 2.40 bug that reports + # incorrectly links instead of Links (Redfish standard) + try: + data_dict.links + return 'links' + except AttributeError: + pass + return 'Links' - def map_links_ref(self): - if self.__version == "0.95": - return "href" - return("@odata.id") + def map_links_ref(self, data_dict=None): + if data_dict == None: + if self.__version == '0.95': + return 'href' + else: + # Checking if we have @odata.id or href. + # This is to deal with proliant firmware 2.40 bug that reports + # incorrectly href instead of @odata.id (Redfish standard) + try: + data_dict.href + return 'href' + except AttributeError: + pass + return '@odata.id' def map_members(self): - if self.__version == "0.95": - return "Member" - return("Members") \ No newline at end of file + if self.__version == '0.95': + return 'Member' + return 'Members' \ No newline at end of file diff --git a/redfish/types.py b/redfish/types.py index d6b3717..fd6f5f0 100644 --- a/redfish/types.py +++ b/redfish/types.py @@ -1,20 +1,22 @@ # coding=utf-8 import pprint +import re from urlparse import urljoin import requests +import simplejson import tortilla import config import mapping -import re +import exception # Global variable class Base(object): - """Abstract class to manage types (Chassis, Servers etc...).""" + '''Abstract class to manage types (Chassis, Servers etc...).''' def __init__(self, url, connection_parameters): - """Class constructor""" + '''Class constructor''' global TORTILLADEBUG self.connection_parameters = connection_parameters # Uggly hack to check self.url = url @@ -28,22 +30,37 @@ class Base(object): headers={'x-auth-token': connection_parameters.auth_token} ) except requests.ConnectionError as e: - print e # Log and transmit the exception. - config.logger.error("Connection error : %s", e) - raise e - print self.data + config.logger.info('Raise a RedfishException to upper level') + msg = 'Connection error : {}\n'.format(e.message) + raise exception.ConnectionFailureException(msg) + except simplejson.scanner.JSONDecodeError as e: + # Log and transmit the exception. + config.logger.info('Raise a RedfishException to upper level') + msg = \ + 'Ivalid content : Content does not appear to be a valid ' + \ + 'Redfish json\n' + raise exception.InvalidRedfishContentException(msg) + except TypeError as e: + # This happen connecting to a manager using non trusted + # SSL certificats. + # The exception is not what could be expected in such case but this + # is the one provided by Tortilla. + config.logger.info('Raise a RedfishException to upper level') + msg = 'Connection error\n' + raise exception.NonTrustedCertificatException(msg) + config.logger.debug(self.data) def get_link_url(self, link_type): - """Need to be explained. + '''Need to be explained. :param redfish_logfile: redfish log :type str :returns: True - """ + ''' self.links=[] - + # Manage standard < 1.0 if float(mapping.redfish_version) < 1.00: links = getattr(self.data, mapping.redfish_mapper.map_links()) @@ -53,7 +70,7 @@ class Base(object): links = getattr(self.data, link_type) link = getattr(links, mapping.redfish_mapper.map_links_ref()) return urljoin(self.url, link) - + @property def url(self): return self.__url @@ -61,58 +78,70 @@ class Base(object): @url.setter def url(self, url): self.__url = url - + def get_parameter(self, parameter_name): - """Generic function to get any system parameter + '''Generic function to get a specific parameter :param parameter_name: name of the parameter :returns: string -- parameter value - - """ + + ''' try: return self.data[parameter_name] except: - return "Parameter does not exist" - + return 'Parameter does not exist' + def get_parameters(self): - """Generic function to get all system parameters + '''Generic function to get all parameters :returns: string -- parameter value - - """ + + ''' try: return self.data except: return -1 - + def set_parameter(self, parameter_name, value): - """Generic function to set any system parameter + '''Generic function to set a specific parameter :param parameter_name: name of the parameter :param value: value to set :returns: string -- http response of PATCH request - - """ + + ''' # Craft the request action = dict() action[parameter_name] = value - print(action) + config.logger.debug(action) # Perform the POST action - print self.api_url - response = self.api_url.patch(verify=self.connection_parameters.verify_cert, - headers={'x-auth-token': self.connection_parameters.auth_token}, - data=action - ) - return response + config.logger.debug(self.api_url) + response = self.api_url.patch( + verify=self.connection_parameters.verify_cert, + headers={'x-auth-token': self.connection_parameters.auth_token}, + data=action) + return response + + def get_name(self): + '''Get root name + + :returns: string -- root name or "Not available" + + ''' + try: + return self.data.Name + except AttributeError: + return "Not available" + class BaseCollection(Base): - """Abstract class to manage collection (Chassis, Servers etc...).""" + '''Abstract class to manage collection (Chassis, Servers etc...).''' def __init__(self, url, connection_parameters): super(BaseCollection, self).__init__(url, connection_parameters) self.links=[] - + #linksmembers = self.data.Links.Members #linksmembers = self.data.links.Member @@ -122,114 +151,189 @@ class BaseCollection(Base): else: linksmembers = getattr(self.data, mapping.redfish_mapper.map_members()) for link in linksmembers: - #self.links.append(getattr(link,"@odata.id")) - #self.links.append(getattr(link,"href")) + #self.links.append(getattr(link,'@odata.id')) + #self.links.append(getattr(link,'href')) self.links.append(urljoin(self.url, getattr(link, mapping.redfish_mapper.map_links_ref()))) - print self.links + config.logger.debug(self.links) class Root(Base): - """Class to manage redfish Root data.""" + '''Class to manage redfish Root data.''' def get_api_version(self): - """Return api version. + '''Return api version. :returns: string -- version :raises: AttributeError - """ + ''' try: version = self.data.RedfishVersion except AttributeError: version = self.data.ServiceVersion - + version = version.replace('.', '') version = version[0] + '.' + version[1:] return(version) def get_api_UUID(self): - """Return UUID version. + '''Return UUID version. :returns: string -- UUID - """ + ''' return self.data.UUID def get_api_link_to_server(self): - """Return api link to server. + '''Return api link to server. :returns: string -- path - """ - return getattr(self.root.Links.Systems, "@odata.id") + ''' + return getattr(self.root.Links.Systems, '@odata.id') class SessionService(Base): - """Class to manage redfish SessionService data.""" + '''Class to manage redfish SessionService data.''' pass class Managers(Base): - """Class to manage redfish Managers.""" + '''Class to manage redfish Managers.''' def __init__(self, url, connection_parameters): super(Managers, self).__init__(url, connection_parameters) try: - -# self.ethernet_interfaces_collection = EthernetInterfacesCollection( -# self.get_link_url("EthernetInterfaces"), -# connection_parameters -# ) - - # Works on proliant, need to treat 095 vs 0.96 differences + # New proliant firmware now respects Redfish v1.00, so seems to correct below statement + # TODO : better handle exception and if possible support old firmware ? self.ethernet_interfaces_collection = EthernetInterfacesCollection( - self.get_link_url("EthernetNICs"), + self.get_link_url('EthernetInterfaces'), connection_parameters ) - except: - pass - - def get_firmware_version(self): - """Get bios version of the system. - :returns: string -- bios version + # Works on proliant, need to treat 095 vs 0.96 differences + #self.ethernet_interfaces_collection = EthernetInterfacesCollection( + # self.get_link_url('EthernetNICs'), + # connection_parameters + # ) + except exception.InvalidRedfishContentException: + # This is to avoid invalid content from the mockup + self.ethernet_interfaces_collection = None - """ + except AttributeError: + # This means we don't have EthernetInterfaces + self.ethernet_interfaces_collection = None + + + def get_firmware_version(self): + '''Get firmware version of the manager + + :returns: string -- bios version or "Not available" + + ''' try: - # Returned by proliant - return self.data.FirmwareVersion - except: - # Returned by mockup. - # Hopefully this kind of discrepencies will be fixed with Redfish 1.0 (August) return self.data.FirmwareVersion + except AttributeError: + # We are here because the attribute could be not defined. + # This is the case with the mockup for manager 2 and 3 + return "Not available" + + def get_type(self): + '''Get manager type + + :returns: string -- manager type or "Not available" + + ''' + try: + return self.data.ManagerType + except AttributeError: + return "Not available" + + def get_uuid(self): + '''Get manager type + + :returns: string -- manager uuid or "Not available" + + ''' + try: + return self.data.UUID + except AttributeError: + return "Not available" + + def get_status(self): + '''Get manager status + + :returns: string -- manager status or "Not available" + + ''' + try: + return self.data.Status.State + except AttributeError: + return "Not available" + + def get_managed_chassis(self): + '''Get managed chassis ids by the manager + + :returns: list -- chassis ids or "Not available" + + ''' + chassis_list = [] + links = getattr(self.data, mapping.redfish_mapper.map_links(self.data)) + + try: + for chassis in links.ManagerForChassis: + result = re.search(r'Chassis/(\w+)', chassis[mapping.redfish_mapper.map_links_ref(chassis)]) + chassis_list.append(result.group(1)) + return chassis_list + except AttributeError: + return "Not available" + + def get_managed_systems(self): + '''Get managed systems ids by the manager + + :returns: list -- chassis ids or "Not available" + + ''' + systems_list = [] + links = getattr(self.data, mapping.redfish_mapper.map_links(self.data)) + + try: + for systems in links.ManagerForServers: + result = re.search(r'Systems/(\w+)', systems[mapping.redfish_mapper.map_links_ref(systems)]) + systems_list.append(result.group(1)) + return systems_list + except AttributeError: + return "Not available" class ManagersCollection(BaseCollection): - """Class to manage redfish ManagersCollection data.""" + '''Class to manage redfish ManagersCollection data.''' def __init__(self, url, connection_parameters): - """Class constructor""" + '''Class constructor''' super(ManagersCollection, self).__init__(url, connection_parameters) - self.managers_list = [] + self.managers_dict = {} for link in self.links: - self.managers_list.append(Managers(link, connection_parameters)) + index = re.search(r'Managers/(\w+)', link) + self.managers_dict[index.group(1)] = Managers(link, connection_parameters) + class Systems(Base): - """Class to manage redfish Systems data.""" + '''Class to manage redfish Systems data.''' # TODO : Need to discuss with Bruno the required method. # Also to check with the ironic driver requirement. def __init__(self, url, connection_parameters): - """Class constructor""" + '''Class constructor''' super(Systems, self).__init__(url, connection_parameters) try: - self.bios = Bios(url + "Bios/Settings", connection_parameters) + self.bios = Bios(url + 'Bios/Settings', connection_parameters) except: pass - + def reset_system(self): - """Force reset of the system. + '''Force reset of the system. :returns: string -- http response of POST request - - """ + + ''' # Craft the request action = dict() action['Action'] = 'Reset' @@ -242,14 +346,14 @@ class Systems(Base): data=action ) #TODO : treat response. - return response - + return response + def get_bios_version(self): - """Get bios version of the system. + '''Get bios version of the system. :returns: string -- bios version - - """ + + ''' try: # Returned by proliant return self.data.Bios.Current.VersionString @@ -259,37 +363,37 @@ class Systems(Base): return self.data.BiosVersion def get_serial_number(self): - """Get serial number of the system. + '''Get serial number of the system. :returns: string -- serial number - - """ + + ''' try: # Returned by proliant return self.data.SerialNumber except: # Returned by mockup. # Hopefully this kind of discrepencies will be fixed with Redfish 1.0 (August) - return "" - + return '' + def get_power(self): - """Get power status of the system. + '''Get power status of the system. :returns: string -- power status or NULL if there is an issue - - """ + + ''' try: return self.data.Power except: - return "" + return '' def set_parameter_json(self, value): - """Generic function to set any system parameter using json structure + '''Generic function to set any system parameter using json structure :param value: json structure with value to update :returns: string -- http response of PATCH request - - """ + + ''' # perform the POST action #print self.api_url.url() response = requests.patch(self.api_url.url(), @@ -297,9 +401,9 @@ class Systems(Base): headers={'x-auth-token': self.connection_parameters.auth_token, 'Content-type': 'application/json'}, data=value) return response.reason - + def set_boot_source_override(self, target, enabled): - """Shotcut function to set boot source + '''Shotcut function to set boot source :param target: new boot source. Supported values: "None", @@ -318,43 +422,107 @@ class Systems(Base): "Once", "Continuous" :returns: string -- http response of PATCH request - """ + ''' return self.set_parameter_json('{"Boot": {"BootSourceOverrideTarget": "'+target+'"},{"BootSourceOverrideEnabled" : "'+enabled+'"}}') + class SystemsCollection(BaseCollection): - """Class to manage redfish ManagersCollection data.""" + '''Class to manage redfish SystemsCollection data.''' def __init__(self, url, connection_parameters): super(SystemsCollection, self).__init__(url, connection_parameters) - + self.systems_list = [] - + for link in self.links: self.systems_list.append(Systems(link, connection_parameters)) + class Bios(Base): - """Class to manage redfish Bios data.""" + '''Class to manage redfish Bios data.''' def __init__(self, url, connection_parameters): super(Bios, self).__init__(url, connection_parameters) - self.boot = Boot(re.findall(".+/Bios",url)[0]+"/Boot/Settings", connection_parameters) + self.boot = Boot(re.findall('.+/Bios', url)[0] + '/Boot/Settings', connection_parameters) + class Boot(Base): - """Class to manage redfish Boot data.""" + '''Class to manage redfish Boot data.''' def __init__(self, url, connection_parameters): super(Boot, self).__init__(url, connection_parameters) - + + class EthernetInterfacesCollection(BaseCollection): - """Class to manage redfish EthernetInterfacesColkection data.""" + '''Class to manage redfish EthernetInterfacesColkection data.''' def __init__(self, url, connection_parameters): super(EthernetInterfacesCollection, self).__init__(url, connection_parameters) - - self.ethernet_interfaces_list = [] - - # Url returned by the mock up is wrong /redfish/v1/Managers/EthernetInterfaces/1 returns a 404. - # The correct one should be /redfish/v1/Managers/1/EthernetInterfaces/1 + + self.ethernet_interfaces_dict = {} + + # Url returned by the mock up is wrong /redfish/v1/Managers/EthernetInterfaces/1 returns a 404. --> this is not true anymore (2016/01/03) + # The correct one should be /redfish/v1/Managers/1/EthernetInterfaces/1 --> correct by mockup return invalid content (not json) # Check more than 1 hour for this bug.... grrr.... for link in self.links: - self.ethernet_interfaces_list.append(EthernetInterfaces(link, connection_parameters)) + index = re.search(r'EthernetInterfaces/(\w+)', link) + self.ethernet_interfaces_dict[index.group(1)] = EthernetInterfaces(link, connection_parameters) + class EthernetInterfaces(Base): - """Class to manage redfish EthernetInterfaces data.""" - pass \ No newline at end of file + '''Class to manage redfish EthernetInterfaces.''' + def get_mac(self): + '''Get EthernetInterface MacAddress + + :returns: string -- interface macaddress or "Not available" + + ''' + try: + return self.data.MacAddress + except AttributeError: + return "Not available" + + def get_fqdn(self): + '''Get EthernetInterface fqdn + + :returns: string -- interface fqdn or "Not available" + + ''' + try: + return self.data.FQDN + except AttributeError: + return "Not available" + + + def get_ipv4(self): + '''Get EthernetInterface ipv4 address + + :returns: list -- interface ip addresses or "Not available" + + ''' + + ipaddresses = [] + + try: + for ip_settings in self.data.IPv4Addresses: + address = ip_settings['Address'] + ipaddresses.append(address) + + return ipaddresses + except AttributeError: + return "Not available" + + def get_ipv6(self): + '''Get EthernetInterface ipv6 address + + :returns: list -- interface ip addresses or "Not available" + + ''' + + ipaddresses = [] + + try: + for ip_settings in self.data.IPv6Addresses: + address = ip_settings['Address'] + ipaddresses.append(address) + + return ipaddresses + except AttributeError: + return "Not available" + diff --git a/requirements.txt b/requirements.txt index 8a4779b..01b840b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ pbr>=0.6,!=0.7,<1.0 #oslo.log>=1.0,<2.0 Babel>=1.3 tortilla>=0.4.1 +Jinja2>=2.7.3