Add power management

Change-Id: Iab9d9f7e4e25e3d3fdec9b28fe49a7226e68c9ff
This commit is contained in:
Imre Farkas 2015-09-25 14:44:02 +02:00
parent ef3d9a292e
commit 9153b850b2
12 changed files with 520 additions and 1 deletions

116
dracclient/client.py Normal file
View File

@ -0,0 +1,116 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Wrapper for pywsman.Client
"""
import logging
from dracclient import exceptions
from dracclient.resources import bios
from dracclient import utils
from dracclient import wsman
LOG = logging.getLogger(__name__)
class DRACClient(object):
"""Client for managing DRAC nodes"""
def __init__(self, host, username, password, port=443, path='/wsman',
protocol='https'):
"""Creates client object
:param host: hostname or IP of the DRAC interface
:param username: username for accessing the DRAC interface
:param password: password for accessing the DRAC interface
:param port: port for accessing the DRAC interface
:param path: path for accessing the DRAC interface
:param protocol: protocol for accessing the DRAC interface
"""
self.client = WSManClient(host, username, password, port, path,
protocol)
self._power_mgmt = bios.PowerManagement(self.client)
def get_power_state(self):
"""Returns the current power state of the node
:returns: power state of the node, one of 'POWER_ON', 'POWER_OFF' or
'REBOOT'
:raises: WSManRequestFailure on request failures
:raises: WSManInvalidResponse when receiving invalid response
:raises: DRACOperationFailed on error reported back by the DRAC
interface
"""
return self._power_mgmt.get_power_state()
def set_power_state(self, target_state):
"""Turns the server power on/off or do a reboot
:param target_state: target power state. Valid options are: 'POWER_ON',
'POWER_OFF' and 'REBOOT'.
:raises: WSManRequestFailure on request failures
:raises: WSManInvalidResponse when receiving invalid response
:raises: DRACOperationFailed on error reported back by the DRAC
interface
:raises: DRACUnexpectedReturnValue on return value mismatch
:raises: InvalidParameterValue on invalid target power state
"""
self._power_mgmt.set_power_state(target_state)
class WSManClient(wsman.Client):
"""Wrapper for wsman.Client with return value checking"""
def invoke(self, resource_uri, method, selectors=None, properties=None,
expected_return_value=None):
"""Invokes a remote WS-Man method
:param resource_uri: URI of the resource
:param method: name of the method to invoke
:param selectors: dictionary of selectors
:param properties: dictionary of properties
:param expected_return_value: expected return value reported back by
the DRAC card. For return value codes check the profile
documentation of the resource used in the method call. If not set,
return value checking is skipped.
:returns: an lxml.etree.Element object of the response received
:raises: WSManRequestFailure on request failures
:raises: WSManInvalidResponse when receiving invalid response
:raises: DRACOperationFailed on error reported back by the DRAC
interface
:raises: DRACUnexpectedReturnValue on return value mismatch
"""
if selectors is None:
selectors = {}
if properties is None:
properties = {}
resp = super(WSManClient, self).invoke(resource_uri, method, selectors,
properties)
return_value = utils.find_xml(resp, 'ReturnValue', resource_uri).text
if return_value == utils.RET_ERROR:
message_elems = utils.find_xml(resp, 'Message', resource_uri, True)
messages = [message_elem.text for message_elem in message_elems]
raise exceptions.DRACOperationFailed(drac_messages=messages)
if (expected_return_value is not None and
return_value != expected_return_value):
raise exceptions.DRACUnexpectedReturnValue(
expected_return_value=expected_return_value,
actual_return_value=return_value)
return resp

17
dracclient/constants.py Normal file
View File

@ -0,0 +1,17 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# power states
POWER_ON = 'POWER_ON'
POWER_OFF = 'POWER_OFF'
REBOOT = 'REBOOT'

View File

@ -21,8 +21,26 @@ class BaseClientException(Exception):
super(BaseClientException, self).__init__(message) super(BaseClientException, self).__init__(message)
class DRACRequestFailed(BaseClientException):
pass
class DRACOperationFailed(DRACRequestFailed):
msg_fmt = ('DRAC operation failed. Messages: %(drac_messages)s')
class DRACUnexpectedReturnValue(DRACRequestFailed):
msg_fmt = ('DRAC operation yielded return value %(actual_return_value)s '
'that is neither error nor the expected '
'%(expected_return_value)s')
class InvalidParameterValue(BaseClientException):
msg_fmt = '%(reason)s'
class WSManRequestFailure(BaseClientException): class WSManRequestFailure(BaseClientException):
msg_fmt = ('WSMan request failed.') msg_fmt = ('WSMan request failed')
class WSManInvalidResponse(BaseClientException): class WSManInvalidResponse(BaseClientException):

View File

View File

@ -0,0 +1,84 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from dracclient import constants
from dracclient import exceptions
from dracclient.resources import uris
from dracclient import utils
POWER_STATES = {
'2': constants.POWER_ON,
'3': constants.POWER_OFF,
'11': constants.REBOOT,
}
REVERSE_POWER_STATES = dict((v, k) for (k, v) in POWER_STATES.items())
class PowerManagement(object):
def __init__(self, client):
"""Creates PowerManagement object
:param client: an instance of WSManClient
"""
self.client = client
def get_power_state(self):
"""Returns the current power state of the node
:returns: power state of the node, one of 'POWER_ON', 'POWER_OFF' or
'REBOOT'
:raises: WSManRequestFailure on request failures
:raises: WSManInvalidResponse when receiving invalid response
:raises: DRACOperationFailed on error reported back by the DRAC
interface
"""
filter_query = ('select EnabledState from '
'DCIM_ComputerSystem where Name="srv:system"')
doc = self.client.enumerate(uris.DCIM_ComputerSystem,
filter_query=filter_query)
enabled_state = utils.find_xml(doc, 'EnabledState',
uris.DCIM_ComputerSystem)
return POWER_STATES[enabled_state.text]
def set_power_state(self, target_state):
"""Turns the server power on/off or do a reboot
:param target_state: target power state. Valid options are: 'POWER_ON',
'POWER_OFF' and 'REBOOT'.
:raises: WSManRequestFailure on request failures
:raises: WSManInvalidResponse when receiving invalid response
:raises: DRACOperationFailed on error reported back by the DRAC
interface
:raises: DRACUnexpectedReturnValue on return value mismatch
:raises: InvalidParameterValue on invalid target power state
"""
try:
drac_requested_state = REVERSE_POWER_STATES[target_state]
except KeyError:
msg = ("'%(target_state)s' is not supported. "
"Supported power states: %(supported_power_states)r") % {
'target_state': target_state,
'supported_power_states': list(REVERSE_POWER_STATES)}
raise exceptions.InvalidParameterValue(reason=msg)
selectors = {'CreationClassName': 'DCIM_ComputerSystem',
'Name': 'srv:system'}
properties = {'RequestedState': drac_requested_state}
self.client.invoke(uris.DCIM_ComputerSystem, 'RequestStateChange',
selectors, properties)

View File

@ -0,0 +1,44 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Schema definitions and resource URIs for the classes implemented by the DRAC
WS-Man API.
"""
DCIM_BIOSEnumeration = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2/'
'DCIM_BIOSEnumeration')
DCIM_BIOSInteger = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2/'
'DCIM_BIOSInteger')
DCIM_BIOSService = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2/'
'DCIM_BIOSService')
DCIM_BIOSString = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2/'
'DCIM_BIOSString')
DCIM_BootConfigSetting = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2/'
'DCIM_BootConfigSetting')
DCIM_BootSourceSetting = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2/'
'DCIM_BootSourceSetting')
DCIM_ComputerSystem = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2'
'/DCIM_ComputerSystem')
DCIM_LifecycleJob = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2/'
'DCIM_LifecycleJob')
DCIM_SystemView = ('http://schemas.dell.com/wbem/wscim/1/cim-schema/2/'
'DCIM_SystemView')

View File

@ -0,0 +1,123 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import requests_mock
import dracclient.client
from dracclient import exceptions
from dracclient.resources import uris
from dracclient.tests import base
from dracclient.tests import utils as test_utils
@requests_mock.Mocker()
class ClientPowerManagementTestCase(base.BaseTest):
def setUp(self):
super(ClientPowerManagementTestCase, self).setUp()
self.drac_client = dracclient.client.DRACClient(
**test_utils.FAKE_ENDPOINT)
def test_get_power_state(self, mock_requests):
mock_requests.post(
'https://1.2.3.4:443/wsman',
text=test_utils.BIOSEnumerations[uris.DCIM_ComputerSystem]['ok'])
self.assertEqual('POWER_ON', self.drac_client.get_power_state())
def test_set_power_state(self, mock_requests):
mock_requests.post(
'https://1.2.3.4:443/wsman',
text=test_utils.BIOSInvocations[
uris.DCIM_ComputerSystem]['RequestStateChange']['ok'])
self.assertIsNone(self.drac_client.set_power_state('POWER_ON'))
def test_set_power_state_fail(self, mock_requests):
mock_requests.post(
'https://1.2.3.4:443/wsman',
text=test_utils.BIOSInvocations[
uris.DCIM_ComputerSystem]['RequestStateChange']['error'])
self.assertRaises(exceptions.DRACOperationFailed,
self.drac_client.set_power_state, 'POWER_ON')
def test_set_power_state_invalid_target_state(self, mock_requests):
self.assertRaises(exceptions.InvalidParameterValue,
self.drac_client.set_power_state, 'foo')
@requests_mock.Mocker()
class WSManClientTestCase(base.BaseTest):
def test_enumerate(self, mock_requests):
mock_requests.post('https://1.2.3.4:443/wsman',
text='<result>yay!</result>')
client = dracclient.client.WSManClient(**test_utils.FAKE_ENDPOINT)
resp = client.enumerate('http://resource')
self.assertEqual('yay!', resp.text)
def test_invoke(self, mock_requests):
xml = """
<response xmlns:n1="http://resource">
<n1:ReturnValue>42</n1:ReturnValue>
<result>yay!</result>
</response>
""" # noqa
mock_requests.post('https://1.2.3.4:443/wsman', text=xml)
client = dracclient.client.WSManClient(**test_utils.FAKE_ENDPOINT)
resp = client.invoke('http://resource', 'Foo')
self.assertEqual('yay!', resp.find('result').text)
def test_invoke_with_expected_return_value(self, mock_requests):
xml = """
<response xmlns:n1="http://resource">
<n1:ReturnValue>42</n1:ReturnValue>
<result>yay!</result>
</response>
""" # noqa
mock_requests.post('https://1.2.3.4:443/wsman', text=xml)
client = dracclient.client.WSManClient(**test_utils.FAKE_ENDPOINT)
resp = client.invoke('http://resource', 'Foo',
expected_return_value='42')
self.assertEqual('yay!', resp.find('result').text)
def test_invoke_with_error_return_value(self, mock_requests):
xml = """
<response xmlns:n1="http://resource">
<n1:ReturnValue>2</n1:ReturnValue>
<result>yay!</result>
</response>
""" # noqa
mock_requests.post('https://1.2.3.4:443/wsman', text=xml)
client = dracclient.client.WSManClient(**test_utils.FAKE_ENDPOINT)
self.assertRaises(exceptions.DRACOperationFailed, client.invoke,
'http://resource', 'Foo')
def test_invoke_with_unexpected_return_value(self, mock_requests):
xml = """
<response xmlns:n1="http://resource">
<n1:ReturnValue>42</n1:ReturnValue>
<result>yay!</result>
</response>
""" # noqa
mock_requests.post('https://1.2.3.4:443/wsman', text=xml)
client = dracclient.client.WSManClient(**test_utils.FAKE_ENDPOINT)
self.assertRaises(exceptions.DRACUnexpectedReturnValue, client.invoke,
'http://resource', 'Foo',
expected_return_value='4242')

View File

@ -13,6 +13,8 @@
import os import os
from dracclient.resources import uris
FAKE_ENDPOINT = { FAKE_ENDPOINT = {
'host': '1.2.3.4', 'host': '1.2.3.4',
'port': '443', 'port': '443',
@ -40,3 +42,20 @@ WSManEnumerations = {
load_wsman_xml('wsman-enum_context-4'), load_wsman_xml('wsman-enum_context-4'),
] ]
} }
BIOSEnumerations = {
uris.DCIM_ComputerSystem: {
'ok': load_wsman_xml('computer_system-enum-ok')
},
}
BIOSInvocations = {
uris.DCIM_ComputerSystem: {
'RequestStateChange': {
'ok': load_wsman_xml(
'computer_system-invoke-request_state_change-ok'),
'error': load_wsman_xml(
'computer_system-invoke-request_state_change-error'),
},
},
}

View File

@ -0,0 +1,23 @@
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
xmlns:n1="http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_ComputerSystem">
<s:Header>
<wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
<wsa:Action>http://schemas.xmlsoap.org/ws/2004/09/enumeration/PullResponse</wsa:Action>
<wsa:RelatesTo>uuid:2b1f3c28-1ca3-1ca3-8003-fd0aa2bdb228</wsa:RelatesTo>
<wsa:MessageID>uuid:5ba7c817-1ca7-1ca7-8a31-a36fc6fe83b0</wsa:MessageID>
</s:Header>
<s:Body>
<wsen:PullResponse>
<wsen:Items>
<n1:DCIM_ComputerSystem>
<n1:CreationClassName>DCIM_ComputerSystem</n1:CreationClassName>
<n1:EnabledState>2</n1:EnabledState>
<n1:Name>srv:system</n1:Name>
</n1:DCIM_ComputerSystem>
</wsen:Items>
<wsen:EndOfSequence/>
</wsen:PullResponse>
</s:Body>
</s:Envelope>

View File

@ -0,0 +1,18 @@
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:n1="http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_ComputerSystem">
<s:Header>
<wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
<wsa:Action>http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_ComputerSystem/RequestStateChangeResponse</wsa:Action>
<wsa:RelatesTo>uuid:0e1e69c7-1ca6-1ca6-8002-fd0aa2bdb228</wsa:RelatesTo>
<wsa:MessageID>uuid:3f2d0db0-1caa-1caa-8003-fcc71555dbe0</wsa:MessageID>
</s:Header>
<s:Body>
<n1:RequestStateChange_OUTPUT>
<n1:Message>The command failed to set RequestedState</n1:Message>
<n1:MessageArguments>RequestedState</n1:MessageArguments>
<n1:MessageID>SYS021</n1:MessageID>
<n1:ReturnValue>2</n1:ReturnValue>
</n1:RequestStateChange_OUTPUT>
</s:Body>
</s:Envelope>

View File

@ -0,0 +1,15 @@
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
xmlns:n1="http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_ComputerSystem">
<s:Header>
<wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
<wsa:Action>http://schemas.dell.com/wbem/wscim/1/cim-schema/2/DCIM_ComputerSystem/RequestStateChangeResponse</wsa:Action>
<wsa:RelatesTo>uuid:11302b44-1ca6-1ca6-8002-fd0aa2bdb228</wsa:RelatesTo>
<wsa:MessageID>uuid:4244f4ca-1caa-1caa-8004-fcc71555dbe0</wsa:MessageID>
</s:Header>
<s:Body>
<n1:RequestStateChange_OUTPUT>
<n1:ReturnValue>0</n1:ReturnValue>
</n1:RequestStateChange_OUTPUT>
</s:Body>
</s:Envelope>

42
dracclient/utils.py Normal file
View File

@ -0,0 +1,42 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Common functionalities shared between different DRAC modules.
"""
# ReturnValue constants
RET_SUCCESS = '0'
RET_ERROR = '2'
RET_CREATED = '4096'
def find_xml(doc, item, namespace, find_all=False):
"""Find the first or all elements in an ElementTree object.
:param doc: the element tree object.
:param item: the element name.
:param namespace: the namespace of the element.
:param find_all: Boolean value, if True find all elements, if False
find only the first one. Defaults to False.
:returns: if find_all is False the element object will be returned
if found, None if not found. If find_all is True a list of
element objects will be returned or an empty list if no
elements were found.
"""
query = ('.//{%(namespace)s}%(item)s' % {'namespace': namespace,
'item': item})
if find_all:
return doc.findall(query)
return doc.find(query)