diff --git a/.gitignore b/.gitignore index 1d9192b..57a8ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,7 @@ target/ .pydevproject .settings/ .metadata + +# Vim +*~ +*.sw? diff --git a/LICENSE b/LICENSE index e06d208..8f71f43 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Apache License + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/README.rst b/README.rst index 266abc2..7855714 100644 --- a/README.rst +++ b/README.rst @@ -47,6 +47,12 @@ Developer setup To initialize a local development environment (eg, so you can run unit tests) you should run the following commands:: +Contacts +-------- + +Distribution list : python-redfish@mondorescue.org + + Further References ------------------ diff --git a/examples/docker/Dockerfile b/examples/docker/Dockerfile deleted file mode 100644 index 041ad08..0000000 --- a/examples/docker/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM ubuntu:15.04 -MAINTAINER bruno.cornec@hp.com -ENV DEBIAN_FRONTEND noninterative -ENV http_proxy http://web-proxy.fra.hp.com:8080 -ENV https_proxy http://web-proxy.fra.hp.com:8080 -# Install deps for Redfish mockup -RUN apt-get update -RUN apt-get -y install python-mock python-pip git openssh-client libpython2.7-dev python-oslotest -RUN apt-get -y ansible -RUN useradd -m bruno -RUN chown -R bruno /usr/local -RUN su - bruno -c "git clone https://github.com/bcornec/python-redfish.git ; pip install -r python-redfish/requirements.txt ; cd python-redfish ; python setup.py install -O1" -CMD /bin/bash diff --git a/examples/simple-proliant.py b/examples/simple-proliant.py index 9645b34..a8f5174 100644 --- a/examples/simple-proliant.py +++ b/examples/simple-proliant.py @@ -6,6 +6,7 @@ import os import sys import json import redfish +from time import sleep # Get $HOME environment. HOME = os.getenv('HOME') @@ -32,7 +33,7 @@ remote_mgmt = redfish.connect(URL, USER_NAME, PASSWORD, verify_cert=False) print ("Redfish API version : %s \n" % remote_mgmt.get_api_version()) -# Uncomment following line to reset the blade !!! +# Uncomment following line to reset the blade !!! #remote_mgmt.Systems.systems_list[0].reset_system() # TODO : create an attribute to link the managed system directly @@ -41,5 +42,40 @@ print ("Redfish API version : %s \n" % remote_mgmt.get_api_version()) # remote_mgmt.Systems.systems_list[0] = remote_mgmt.Systems.managed_system print("Bios version : {}\n".format(remote_mgmt.Systems.systems_list[0].get_bios_version())) +print("Serial Number : {}\n".format(remote_mgmt.Systems.systems_list[0].get_serial_number())) +print("Power State : {}\n".format(remote_mgmt.Systems.systems_list[0].get_power())) +print("Parameter 'SystemType' : {}\n".format(remote_mgmt.Systems.systems_list[0].get_parameter("SystemType"))) + +print("Get bios parameters : {}\n".format(remote_mgmt.Systems.systems_list[0].bios.get_parameters())) +print("Get boot parameters : {}\n".format(remote_mgmt.Systems.systems_list[0].bios.boot.get_parameters())) + + +#print("Get bios parameter 'AdminPhone' : {}\n".format(remote_mgmt.Systems.systems_list[0].bios.get_parameter("AdminPhone"))) +#print("Set bios parameter 'AdminPhone' to '' : {}\n".format(remote_mgmt.Systems.systems_list[0].bios.set_parameter("AdminPhone",""))) + + +#Boot server with script +#remote_mgmt.Systems.systems_list[0].bios.set_parameter("Dhcpv4","Enabled") + +remote_mgmt.Systems.systems_list[0].bios.set_parameter("PreBootNetwork", "Auto") +remote_mgmt.Systems.systems_list[0].bios.set_parameter("UefiShellStartup", "Enabled") +remote_mgmt.Systems.systems_list[0].bios.set_parameter("UefiShellStartupLocation", "NetworkLocation") +remote_mgmt.Systems.systems_list[0].bios.set_parameter("UefiShellStartupUrl", "http://10.3.222.88/deploy/startup.nsh") + +#remote_mgmt.Systems.systems_list[0].set_parameter_json('{"Boot": {"BootSourceOverrideTarget": "UefiShell"}}') +# remote_mgmt.Systems.systems_list[0].set_parameter_json('{"Boot": {"BootSourceOverrideEnabled" : "Continuous"}}') +#remote_mgmt.Systems.systems_list[0].set_parameter_json('{"Boot": {"BootSourceOverrideEnabled" : "Once"}}') + +mySystem = remote_mgmt.Systems.systems_list[0] +mySystem.set_boot_source_override("None","Disabled") +#Uncomment the next line to reset the server +#mySystem.reset_system() + + +print("Get manager firmware version : {}\n".format(remote_mgmt.Managers.managers_list[0].get_firmware_version())) +print("Get system Bios version : {}\n".format(remote_mgmt.Systems.systems_list[0].get_bios_version())) + +#Reset of the system is required to apply the changes +#remote_mgmt.Systems.systems_list[0].reset_system() remote_mgmt.logout() diff --git a/examples/simple-simulator.py b/examples/simple-simulator.py index c31a0f7..52d9f18 100644 --- a/examples/simple-simulator.py +++ b/examples/simple-simulator.py @@ -31,8 +31,10 @@ PASSWORD = config["Nodes"]["default"]["password"] remote_mgmt = redfish.connect(URL, USER_NAME, PASSWORD, simulator=True, enforceSSL=False) -print ("Redfish API version : {} \n".format(remote_mgmt.get_api_version())) -print ("UUID : {} \n".format(remote_mgmt.Root.get_api_UUID())) -print ("Bios version : {}\n".format(remote_mgmt.Systems.systems_list[0].get_bios_version())) - +print("Redfish API version : {} \n".format(remote_mgmt.get_api_version())) +print("UUID : {} \n".format(remote_mgmt.Root.get_api_UUID())) +print("System 1 :\n") +print("Bios version : {}\n".format(remote_mgmt.Systems.systems_list[0].get_bios_version())) +print("System 2 :\n") +print("Bios version : {}\n".format(remote_mgmt.Systems.systems_list[1].get_parameter("SerialNumber"))) #print remoteMgmt.get_api_link_to_server() diff --git a/python-redfish.spec b/python-redfish.spec index d6c3171..e60f04d 100644 --- a/python-redfish.spec +++ b/python-redfish.spec @@ -28,7 +28,6 @@ system such as defined by http://www.redfishcertification.org %install %{__python} setup.py install -O1 --skip-build --root %{buildroot} -# TODO: Add examples %files %doc README.rst examples/*.py %dir %{python_sitelib}/redfish diff --git a/redfish-client/redfish-client.py b/redfish-client/redfish-client.py new file mode 100755 index 0000000..c1be6b6 --- /dev/null +++ b/redfish-client/redfish-client.py @@ -0,0 +1,126 @@ +#!/usr/bin/python + +# coding=utf-8 + +""" +redfish-client + +Usage: + redfish-client.py [options] config add [] [] + redfish-client.py [options] config del + redfish-client.py [options] config modify (url | login | password) + redfish-client.py [options] config show + redfish-client.py [options] config showall + 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]. + + +config commands manage the configuration file. + +""" + +import os +import sys +import json +import pprint +import docopt + + +class ConfigFile(object): + def __init__(self, config_file): + 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":{}} + + def save(self): + try: + 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) + + def add_manager(self, manager_name, url, login, password): + 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 + + def get_managers(self): + managers = [] + for manager in self.data['Managers']: + managers += [manager] + return(managers) + + def get_manager_info(self, manager): + 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""" + def __init__(self, message=None, **kwargs): + self.kwargs = kwargs + self.message = message + + +if __name__ == '__main__': + # Functions + def show_manager(all=False): + print("Managers configured :") + for manager in conf_file.get_managers(): + print(manager) + if all == True: + info = conf_file.get_manager_info(manager) + print("\tUrl : {}".format(info['url'])) + print("\tLogin : {}".format(info['login'])) + print("\tPassword : {}".format(info['password'])) + + # Get $HOME environment. + HOME = os.getenv('HOME') + + if HOME == '': + print("$HOME environment variable not set, please check your system") + sys.exit(1) + + arguments = docopt.docopt(__doc__, version='redfish-client 0.1') + print(arguments) + + arguments['--conf_file'] = arguments['--conf_file'].replace('~', HOME) + + conf_file = ConfigFile(arguments['--conf_file']) + + + if arguments['config'] == True: + if arguments['show'] == True: + show_manager() + elif arguments['showall'] == True: + show_manager(True) + elif arguments['add'] == True: + conf_file.add_manager(arguments[''], + arguments[''], + arguments[''], + arguments['password']) + pprint.pprint(conf_file.data) + + conf_file.save() + + + sys.exit(0) diff --git a/redfish/exception.py b/redfish/exception.py index f6f759a..6038480 100644 --- a/redfish/exception.py +++ b/redfish/exception.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- - +import sys +import config class RedfishException(Exception): """Base class for redfish exceptions""" @@ -8,9 +9,13 @@ class RedfishException(Exception): self.message = message - class AuthenticationFailureException(RedfishException): - pass - + 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) + class LogoutFailureException(RedfishException): - pass \ No newline at end of file + pass diff --git a/redfish/main.py b/redfish/main.py index 5736732..fbe9f21 100644 --- a/redfish/main.py +++ b/redfish/main.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. + """ STARTING ASSUMPTIONS @@ -285,6 +286,11 @@ class RedfishConnection(object): url = self.Root.get_link_url( mapping.redfish_mapper.map_sessionservice() ) + + # 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} @@ -307,7 +313,7 @@ class RedfishConnection(object): # TODO : Manage exception with a class. # ======================================================================= if auth.status_code != 201: - pass + raise exception.AuthenticationFailureException("Login request return an invalid status code") #sysraise "Error getting token", auth.status_code self.connection_parameters.auth_token = auth.headers.get("x-auth-token") diff --git a/redfish/types.py b/redfish/types.py index d462731..d6b3717 100644 --- a/redfish/types.py +++ b/redfish/types.py @@ -6,14 +6,15 @@ import requests import tortilla import config import mapping +import re # Global variable class Base(object): """Abstract class to manage types (Chassis, Servers etc...).""" - def __init__(self, url, connection_parameters): + """Class constructor""" global TORTILLADEBUG self.connection_parameters = connection_parameters # Uggly hack to check self.url = url @@ -60,11 +61,53 @@ 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 + :param parameter_name: name of the parameter + :returns: string -- parameter value + + """ + try: + return self.data[parameter_name] + except: + return "Parameter does not exist" + + def get_parameters(self): + """Generic function to get all system 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 + + :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) + + # 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 class BaseCollection(Base): """Abstract class to manage collection (Chassis, Servers etc...).""" - def __init__(self, url, connection_parameters): super(BaseCollection, self).__init__(url, connection_parameters) @@ -89,8 +132,6 @@ class BaseCollection(Base): class Root(Base): """Class to manage redfish Root data.""" - - def get_api_version(self): """Return api version. @@ -108,8 +149,12 @@ class Root(Base): return(version) def get_api_UUID(self): - return self.data.UUID + """Return UUID version. + :returns: string -- UUID + + """ + return self.data.UUID def get_api_link_to_server(self): """Return api link to server. @@ -126,9 +171,9 @@ class SessionService(Base): class Managers(Base): + """Class to manage redfish Managers.""" def __init__(self, url, connection_parameters): super(Managers, self).__init__(url, connection_parameters) - try: # self.ethernet_interfaces_collection = EthernetInterfacesCollection( @@ -143,41 +188,68 @@ class Managers(Base): ) except: pass + + def get_firmware_version(self): + """Get bios version of the system. + :returns: string -- bios version + + """ + 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 class ManagersCollection(BaseCollection): """Class to manage redfish ManagersCollection data.""" def __init__(self, url, connection_parameters): + """Class constructor""" super(ManagersCollection, self).__init__(url, connection_parameters) - self.managers_list = [] - for link in self.links: self.managers_list.append(Managers(link, connection_parameters)) - - class Systems(Base): + """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""" super(Systems, self).__init__(url, connection_parameters) + try: + self.bios = Bios(url + "Bios/Settings", connection_parameters) + except: + pass def reset_system(self): + """Force reset of the system. + + :returns: string -- http response of POST request + + """ # Craft the request action = dict() action['Action'] = 'Reset' action['ResetType'] = 'ForceRestart' - # perform the POST action - print self.api_url + #Debug the url and perform the POST action + #print self.api_url response = self.api_url.post(verify=self.connection_parameters.verify_cert, headers={'x-auth-token': self.connection_parameters.auth_token}, data=action - ) + ) #TODO : treat response. - + return response + def get_bios_version(self): + """Get bios version of the system. + + :returns: string -- bios version + + """ try: # Returned by proliant return self.data.Bios.Current.VersionString @@ -186,6 +258,68 @@ class Systems(Base): # Hopefully this kind of discrepencies will be fixed with Redfish 1.0 (August) return self.data.BiosVersion + def get_serial_number(self): + """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 "" + + def get_power(self): + """Get power status of the system. + + :returns: string -- power status or NULL if there is an issue + + """ + try: + return self.data.Power + except: + return "" + + def set_parameter_json(self, value): + """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(), + verify=self.connection_parameters.verify_cert, + 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 + + :param target: new boot source. Supported values: + "None", + "Pxe", + "Floppy", + "Cd", + "Usb", + "Hdd", + "BiosSetup", + "Utilities", + "Diags", + "UefiShell", + "UefiTarget" + :param enabled: Supported values: + "Disabled", + "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.""" @@ -196,9 +330,20 @@ class SystemsCollection(BaseCollection): for link in self.links: self.systems_list.append(Systems(link, connection_parameters)) - - + +class Bios(Base): + """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) + +class Boot(Base): + """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.""" def __init__(self, url, connection_parameters): super(EthernetInterfacesCollection, self).__init__(url, connection_parameters) @@ -210,6 +355,6 @@ class EthernetInterfacesCollection(BaseCollection): for link in self.links: self.ethernet_interfaces_list.append(EthernetInterfaces(link, connection_parameters)) - class EthernetInterfaces(Base): + """Class to manage redfish EthernetInterfaces data.""" pass \ No newline at end of file