diff --git a/xCAT-openbmc-py/lib/python/agent/common/rest.py b/xCAT-openbmc-py/lib/python/agent/common/rest.py index 46a9f7b2e..df9b81b6f 100644 --- a/xCAT-openbmc-py/lib/python/agent/common/rest.py +++ b/xCAT-openbmc-py/lib/python/agent/common/rest.py @@ -18,12 +18,13 @@ class RestSession(object): self.session = requests.Session() self.cookies = None - def request(self, method, url, headers, data=None, timeout=30): + def request(self, method, url, headers, authType=None, data=None, timeout=30): try: response = self.session.request(method, url, data=data, headers=headers, + auth=authType, verify=False, timeout=timeout) except requests.exceptions.ConnectionError as e: diff --git a/xCAT-openbmc-py/lib/python/agent/hwctl/executor/redfish_power.py b/xCAT-openbmc-py/lib/python/agent/hwctl/executor/redfish_power.py new file mode 100644 index 000000000..dafe1209f --- /dev/null +++ b/xCAT-openbmc-py/lib/python/agent/hwctl/executor/redfish_power.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +############################################################################### +# IBM(c) 2018 EPL license http://www.eclipse.org/legal/epl-v10.html +############################################################################### +# -*- coding: utf-8 -*- +# +from __future__ import print_function +import gevent +import time + +from common.task import ParallelNodesCommand +from common.exceptions import SelfClientException, SelfServerException +from hwctl import redfish_client as redfish + +import logging +logger = logging.getLogger('xcatagent') + +POWER_STATE_DB = { + "on" : "powering-on", + "off" : "powering-off", + "softoff" : "powering-off", + "boot" : "powering-on", + "reset" : "powering-on", +} + +class RedfishPowerTask(ParallelNodesCommand): + """Executor for power-related actions.""" + + def get_state(self, **kw): + + node = kw['node'] + rf = redfish.RedfishRest(name=node, nodeinfo=kw['nodeinfo'], messager=self.callback, + debugmode=self.debugmode, verbose=self.verbose) + + state = 'Unknown' + try: + rf.login() + state = rf.get_power_state() + self.callback.info('%s: %s' % (node, state)) + except (SelfServerException, SelfClientException) as e: + self.callback.error(e.message, node) + + return state + + def get_bmcstate (self, **kw): + + node = kw['node'] + rf = redfish.RedfishRest(name=node, nodeinfo=kw['nodeinfo'], messager=self.callback, + debugmode=self.debugmode, verbose=self.verbose) + + state = 'Unknown' + try: + rf.login() + state = rf.get_bmc_state() + self.callback.info('%s: %s' % (node, state)) + except (SelfServerException, SelfClientException) as e: + self.callback.error(e.message, node) + return state + + def set_state(self, state, **kw): + + node = kw['node'] + rf = redfish.RedfishRest(name=node, nodeinfo=kw['nodeinfo'], messager=self.callback, + debugmode=self.debugmode, verbose=self.verbose) + + try: + rf.login() + rf.set_power_state(state) + new_status = POWER_STATE_DB.get(state, '') + self.callback.info('%s: %s' % (node, state)) + if new_status: + self.callback.update_node_attributes('status', node, new_status) + except (SelfServerException, SelfClientException) as e: + self.callback.error(e.message, node) + + def rebootbmc(self, optype='warm', **kw): + + node = kw['node'] + rf = redfish.RedfishRest(name=node, nodeinfo=kw['nodeinfo'], messager=self.callback, + debugmode=self.debugmode, verbose=self.verbose) + + try: + rf.login() + except (SelfServerException, SelfClientException) as e: + return self.callback.error(e.message, node) + + try: + rf.reboot_bmc(optype) + except (SelfServerException, SelfClientException) as e: + self.callback.error(e.message, node) + else: + self.callback.info('%s: %s' % (node, 'bmcreboot')) diff --git a/xCAT-openbmc-py/lib/python/agent/hwctl/executor/redfish_setboot.py b/xCAT-openbmc-py/lib/python/agent/hwctl/executor/redfish_setboot.py new file mode 100644 index 000000000..3ea4ee436 --- /dev/null +++ b/xCAT-openbmc-py/lib/python/agent/hwctl/executor/redfish_setboot.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +############################################################################### +# IBM(c) 2018 EPL license http://www.eclipse.org/legal/epl-v10.html +############################################################################### +# -*- coding: utf-8 -*- +# +from __future__ import print_function +import gevent +import time + +from common.task import ParallelNodesCommand +from common.exceptions import SelfClientException, SelfServerException +from hwctl import redfish_client as redfish + +import logging +logger = logging.getLogger('xcatagent') + +class RedfishBootTask(ParallelNodesCommand): + """Executor for setboot-related actions.""" + + def get_state(self, **kw): + + node = kw['node'] + rf = redfish.RedfishRest(name=node, nodeinfo=kw['nodeinfo'], messager=self.callback, + debugmode=self.debugmode, verbose=self.verbose) + + state = 'Unknown' + try: + rf.login() + state = rf.get_boot_state() + self.callback.info('%s: %s' % (node, state)) + + except (SelfServerException, SelfClientException) as e: + self.callback.error(e.message, node) + + return state + + def set_state(self, state, persistant, **kw): + + node = kw['node'] + rf = redfish.RedfishRest(name=node, nodeinfo=kw['nodeinfo'], messager=self.callback, + debugmode=self.debugmode, verbose=self.verbose) + + state = 'Unknown' + try: + rf.login() + rf.set_boot_state(persistant, state) + state = rf.get_boot_state() + self.callback.info('%s: %s' % (node, state)) + + except (SelfServerException, SelfClientException) as e: + self.callback.error(e.message, node) diff --git a/xCAT-openbmc-py/lib/python/agent/hwctl/redfish_client.py b/xCAT-openbmc-py/lib/python/agent/hwctl/redfish_client.py new file mode 100644 index 000000000..de1292e4b --- /dev/null +++ b/xCAT-openbmc-py/lib/python/agent/hwctl/redfish_client.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +############################################################################### +# IBM(c) 2018 EPL license http://www.eclipse.org/legal/epl-v10.html +############################################################################### +# -*- coding: utf-8 -*- +# + +import os +import requests +import json +import time +from requests.auth import AuthBase + +from common import utils, rest +from common.exceptions import SelfClientException, SelfServerException + +import logging +logger = logging.getLogger('xcatagent') + +HTTP_PROTOCOL = "https://" +PROJECT_URL = "/redfish/v1" + +CHASSIS_URL = PROJECT_URL + "/Chassis" +MANAGER_URL = PROJECT_URL + "/Managers" +SYSTEMS_URL = PROJECT_URL + "/Systems" +SESSION_URL = PROJECT_URL + "/SessionService/Sessions" + +BMC_RESET_TYPE = "ForceRestart" + +POWER_RESET_TYPE = { + 'boot' : 'ForceRestart', + 'off' : 'ForceOff', + 'on' : 'ForceOn', + 'softoff' : 'GracefulShutdown', + 'reset' : 'GracefulRestart' +} + +BOOTSOURCE_SET_STATE = { + "cd" : "Cd", + "def" : "None", + "floppy": "Floppy", + "hd" : 'Hdd', + "net" : "Pxe", + "setup" : "BiosSetup", +} + +BOOTSOURCE_GET_STATE = { + "BiosSetup": "BIOS Setup", + "Floppy" : "Floppy", + "Cd" : "CD/DVD", + "Hdd" : "Hard Disk", + "None" : "BIOS default", + "Pxe" : "Network", +} + +class RedfishRest(object): + + headers = {'Content-Type': 'application/json'} + + def __init__(self, name, **kwargs): + + self.name = name + self.username = None + self.password = None + + if 'nodeinfo' in kwargs: + for key, value in kwargs['nodeinfo'].items(): + setattr(self, key, value) + if not hasattr(self, 'bmcip'): + self.bmcip = self.name + + self.verbose = kwargs.get('debugmode') + self.messager = kwargs.get('messager') + + self.session = rest.RestSession() + self.auth = None + self.root_url = HTTP_PROTOCOL + self.bmcip + + def _print_record_log (self, msg, cmd, error_flag=False): + + if self.verbose or error_flag: + localtime = time.asctime( time.localtime(time.time()) ) + log = self.name + ': [redfish_debug] ' + cmd + ' ' + msg + if self.verbose: + self.messager.info(localtime + ' ' + log) + logger.debug(log) + + def _print_error_log (self, msg, cmd): + + self._print_record_log(msg, cmd, True) + + def _log_request (self, method, url, headers, data=None, files=None, file_path=None, cmd=''): + + header_str = ' '.join([ "%s: %s" % (k, v) for k,v in headers.items() ]) + msg = 'curl -k -X %s -H \"%s\" ' % (method, header_str) + + if cmd != 'login': + msg += '-H \"X-Auth-Token: xxxxxx\" ' + + if data: + if cmd == 'login': + data = data.replace('"Password": "%s"' % self.password, '"Password": "xxxxxx"') + data += ' -v' + msg += '%s -d \'%s\'' % (url, data) + else: + msg += url + + self._print_record_log(msg, cmd) + return msg + + def request (self, method, resource, headers=None, payload=None, timeout=30, cmd=''): + + httpheaders = headers or RedfishRest.headers + url = resource + if not url.startswith(HTTP_PROTOCOL): + url = self.root_url + resource + + data = None + if payload: + data=json.dumps(payload) + + self._log_request(method, url, httpheaders, data=data, cmd=cmd) + try: + response = self.session.request(method, url, httpheaders, authType=self.auth, data=data, timeout=timeout) + return self.handle_response(response, cmd=cmd) + except SelfServerException as e: + if cmd == 'login': + e.message = "Login to BMC failed: Can't connect to {0} {1}.".format(e.host_and_port, e.detail_msg) + else: + e.message = 'BMC did not respond. ' \ + 'Validate BMC configuration and retry the command.' + self._print_error_log(e.message, cmd) + raise + except ValueError: + error = 'Received wrong format response: %s' % response + self._print_error_log(error, cmd) + raise SelfServerException(error) + + def handle_response (self, resp, cmd=''): + + data = resp.json() + code = resp.status_code + + if code != requests.codes.ok and code != requests.codes.created: + + description = ''.join(data['error']['@Message.ExtendedInfo'][0]['Message']) + error = '[%d] %s' % (code, description) + self._print_error_log(error, cmd) + raise SelfClientException(error, code) + + if cmd == 'login' and not 'X-Auth-Token' in resp.headers: + raise SelfServerException('Login Failed: Did not get Session Token from response') + + if not self.auth: + self.auth = RedfishAuth(resp.headers['X-Auth-Token']) + + self._print_record_log('%s %s' % (code, data['Name']), cmd) + return data + + def login(self): + + payload = { "UserName": self.username, "Password": self.password } + self.request('POST', SESSION_URL, payload=payload, timeout=20, cmd='login') + + def _get_members(self, url): + + data = self.request('GET', url, cmd='get_members') + try: + return data['Members'] + except KeyError as e: + raise SelfServerException('Get KeyError %s' % e.message) + + def get_bmc_state(self): + + members = self._get_members(MANAGER_URL) + target_url = members[0]['@odata.id'] + data = self.request('GET', target_url, cmd='get_bmc_state') + try: + return data['PowerState'] + except KeyError as e: + raise SelfServerException('Get KeyError %s' % e.message) + + def get_power_state(self): + + members = self._get_members(CHASSIS_URL) + target_url = members[0]['@odata.id'] + data = self.request('GET', target_url, cmd='get_power_state') + try: + return data['PowerState'] + except KeyError as e: + raise SelfServerException('Get KeyError %s' % e.message) + + def _get_bmc_actions(self): + + members = self._get_members(MANAGER_URL) + target_url = members[0]['@odata.id'] + data = self.request('GET', target_url, cmd='get_bmc_actions') + try: + actions = data['Actions']['#Manager.Reset']['ResetType@Redfish.AllowableValues'] + target_url = data['Actions']['#Manager.Reset']['target'] + except KeyError as e: + raise SelfServerException('Get KeyError %s' % e.message) + + return (target_url, actions) + + def reboot_bmc(self, optype='warm'): + + target_url, actions = self._get_bmc_actions() + if BMC_RESET_TYPE not in actions: + raise SelfClientException('Unsupport option: %s' % BMC_RESET_TYPE) + + data = { "ResetType": BMC_RESET_TYPE } + payload = json.dumps(data) + return self.request('POST', target_url, payload=payload, cmd='set_bmc_state') + + def _get_power_actions(self): + + members = self._get_members(CHASSIS_URL) + target_url = members[0]['@odata.id'] + data = self.request('GET', target_url, cmd='get_power_actions') + try: + actions = data['Actions']['#ComputerSystem.Reset']['ResetType@Redfish.AllowableValues'] + target_url = data['Actions']['#ComputerSystem.Reset']['target'] + except KeyError as e: + raise SelfServerException('Get KeyError %s' % e.message) + + return (target_url, actions) + + def set_power_state(self, state): + + target_url, actions = self._get_power_actions() + if POWER_RESET_TYPE[state] not in actions: + raise SelfClientException('Unsupport option: %s' % state) + + data = { "ResetType": state } + payload = json.dumps(data) + return self.request('POST', target_url, payload=payload, cmd='set_power_state') + + def get_boot_state(self): + + members = self._get_members(SYSTEMS_URL) + target_url = members[0]['@odata.id'] + data = self.request('GET', target_url, cmd='get_boot_state') + try: + boot_enable = data['Boot']['BootSourceOverrideEnabled'] + if boot_enable == 'None': + return 'boot override inactive' + bootsource = data['Boot']['BootSourceOverrideTarget'] + return BOOTSOURCE_GET_STATE.get(bootsource, bootsource) + except KeyError as e: + raise SelfServerException('Get KeyError %s' % e.message) + + def _get_boot_actions(self): + + members = self._get_members(SYSTEMS_URL) + target_url = members[0]['@odata.id'] + data = self.request('GET', target_url, cmd='get_boot_actions') + try: + actions = data['Boot']['BootSourceOverrideTarget@Redfish.AllowableValues'] + except KeyError as e: + raise SelfServerException('Get KeyError %s' % e.message) + + return (target_url, actions) + + def set_boot_state(self, persistant, state): + + target_url, actions = self._get_boot_actions() + target_data = BOOTSOURCE_SET_STATE[state] + if target_data not in actions: + raise SelfClientException('Unsupport option: %s' % state) + + boot_enable = 'Once' + if persistant: + boot_enable = 'Continuous' + data = {'Boot': {'BootSourceOverrideEnabled': boot_enable, "BootSourceOverrideTarget": target_data} } + json.dumps(data) + return self.request('PATCH', target_url, payload=payload, cmd='set_boot_state') + +class RedfishAuth(AuthBase): + + def __init__(self,authToken): + + self.authToken=authToken + + def __call__(self, auth): + auth.headers['X-Auth-Token']=self.authToken + return(auth) diff --git a/xCAT-openbmc-py/lib/python/agent/xcatagent/base.py b/xCAT-openbmc-py/lib/python/agent/xcatagent/base.py index d75bd7f00..e9b29982e 100644 --- a/xCAT-openbmc-py/lib/python/agent/xcatagent/base.py +++ b/xCAT-openbmc-py/lib/python/agent/xcatagent/base.py @@ -2,7 +2,8 @@ from common import utils import gevent from gevent.pool import Pool -MODULE_MAP = {"openbmc": "OpenBMCManager"} +MODULE_MAP = {"openbmc": "OpenBMCManager", + "redfish": "RedfishManager"} class BaseManager(object): def __init__(self, messager, cwd): diff --git a/xCAT-openbmc-py/lib/python/agent/xcatagent/redfish.py b/xCAT-openbmc-py/lib/python/agent/xcatagent/redfish.py new file mode 100644 index 000000000..51fdc26b0 --- /dev/null +++ b/xCAT-openbmc-py/lib/python/agent/xcatagent/redfish.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +############################################################################### +# IBM(c) 2018 EPL license http://www.eclipse.org/legal/epl-v10.html +############################################################################### +# -*- coding: utf-8 -*- +# + +import os +import gevent +import re +import sys +from docopt import docopt,DocoptExit + +from common import utils +from common import exceptions as xcat_exception +from hwctl.executor.redfish_power import RedfishPowerTask +from hwctl.executor.redfish_setboot import RedfishBootTask +from hwctl.power import DefaultPowerManager +from hwctl.setboot import DefaultBootManager + +from xcatagent import base +import logging +logger = logging.getLogger('xcatagent') +try: + if not logger.handlers: + utils.enableSyslog('xcat.agent') +except: + pass + +DEBUGMODE = False +VERBOSE = False + +# global variables of rpower +POWER_REBOOT_OPTIONS = ('boot', 'reset') +POWER_SET_OPTIONS = ('on', 'off', 'bmcreboot', 'softoff') +POWER_GET_OPTIONS = ('bmcstate', 'state', 'stat', 'status') + +# global variables of rsetboot +SETBOOT_GET_OPTIONS = ('stat', '') +SETBOOT_SET_OPTIONS = ('cd', 'def', 'default', 'floppy', 'hd', 'net', 'setup') + +class RedfishManager(base.BaseManager): + def __init__(self, messager, cwd, nodes=None, envs=None): + super(RedfishManager, self).__init__(messager, cwd) + self.nodes = nodes + self.debugmode = (envs and envs.get('debugmode')) or None + #TODO, remove the global variable DEBUGMODE + global DEBUGMODE + DEBUGMODE = envs['debugmode'] + + if self.debugmode: + logger.setLevel(logging.DEBUG) + + def rpower(self, nodesinfo, args): + + # 1, parse args + rpower_usage = """ + Usage: + rpower [-V|--verbose] [boot|bmcreboot|bmcstate|off|on|reset|softoff|stat|state|status] + + Options: + -V --verbose rpower verbose mode. + """ + + try: + opts=docopt(rpower_usage, argv=args) + + self.verbose=opts.pop('--verbose') + action=[k for k,v in opts.items() if v][0] + except Exception as e: + # It will not be here as perl has validation for args + self.messager.error("Failed to parse arguments for rpower: %s" % args) + return + + # 2, validate the args + if action not in (POWER_GET_OPTIONS + POWER_SET_OPTIONS + POWER_REBOOT_OPTIONS): + self.messager.error("Not supported subcommand for rpower: %s" % action) + return + + # 3, run the subcommands + runner = RedfishPowerTask(nodesinfo, callback=self.messager, debugmode=self.debugmode, verbose=self.verbose) + if action == 'bmcstate': + DefaultPowerManager().get_bmc_state(runner) + elif action == 'bmcreboot': + DefaultPowerManager().reboot_bmc(runner) + elif action in POWER_GET_OPTIONS: + DefaultPowerManager().get_power_state(runner) + else: + DefaultPowerManager().set_power_state(runner, power_state=action) + + + def rsetboot(self, nodesinfo, args): + + # 1, parse args + if not args: + args = ['stat'] + + rsetboot_usage = """ + Usage: + rsetboot [-V|--verbose] [cd|def|default|floppy||hd|net|stat|setup] [-p] + + Options: + -V --verbose rsetboot verbose mode. + -p persistant boot source. + """ + + try: + opts = docopt(rsetboot_usage, argv=args) + + self.verbose = opts.pop('--verbose') + action_type = opts.pop('-p') + action = [k for k,v in opts.items() if v][0] + except Exception as e: + self.messager.error("Failed to parse arguments for rsetboot: %s" % args) + return + + # 2, validate the args + if action not in (SETBOOT_GET_OPTIONS + SETBOOT_SET_OPTIONS): + self.messager.error("Not supported subcommand for rsetboot: %s" % action) + return + + # 3, run the subcommands + runner = RedfishBootTask(nodesinfo, callback=self.messager, debugmode=self.debugmode, verbose=self.verbose) + if action in SETBOOT_GET_OPTIONS: + DefaultBootManager().get_boot_state(runner) + else: + DefaultBootManager().set_boot_state(runner, setboot_state=action, persistant=action_type) diff --git a/xCAT-server/lib/xcat/plugins/redfish.pm b/xCAT-server/lib/xcat/plugins/redfish.pm index 752a8a9b3..b98a342f4 100644 --- a/xCAT-server/lib/xcat/plugins/redfish.pm +++ b/xCAT-server/lib/xcat/plugins/redfish.pm @@ -152,13 +152,29 @@ sub parse_args { return ([ 1, "Error parsing arguments." ]); } - if (scalar(@ARGV) != 1 and ($command =~ /rpower/)) { + if (scalar(@ARGV) >= 2 and ($command =~ /rbeacon|rpower|rvitals/)) { return ([ 1, "Only one option is supported at the same time for $command" ]); + } elsif (scalar(@ARGV) == 0 and $command =~ /rbeacon|rspconfig|rpower/) { + return ([ 1, "No option specified for $command" ]); } else { $subcommand = $ARGV[0]; } - return ([ 1, "Unsupported any command for redfish now" ]); + if ($command eq "rpower") { + unless ($subcommand =~ /^bmcstate$|^status$|^stat$|^state$/) { + return ([ 1, "Unsupported command: $command $subcommand" ]); + } + } elsif ($command eq "rsetboot") { + my $persistant; + GetOptions('p' => \$persistant); + return ([ 1, "Only one option is supported at the same time for $command" ]) if (@ARGV > 1); + $subcommand = "stat" if (!defined($ARGV[0])); + unless ($subcommand =~ /^stat$/) { + return ([ 1, "Unsupported command: $command $subcommand" ]); + } + } else { + return ([ 1, "Unsupported command: $command" ]); + } } #-------------------------------------------------------