Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ jobs:
- name: Lint with ruff
run: ruff check --output-format=github .

mypy:
name: Type check with mypy
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install dependencies
run: pip install mypy==2.4.0

# the files and options are configured in setup.cfg
- name: Type check with mypy
run: mypy

codespell:
name: Check spelling with codespell
runs-on: ubuntu-24.04
Expand Down
10 changes: 9 additions & 1 deletion man/ipmitool.py.1
Original file line number Diff line number Diff line change
Expand Up @@ -401,13 +401,21 @@ Request the power channel status
starting power channel

.SH COMMAND \fI\,'ipmitool.py picmg channel power'\/\fR
usage: ipmitool.py picmg channel power [\-h] channel
usage: ipmitool.py picmg channel power [\-h] channel {on,off} current_limit

Send channel power

.TP
\fBchannel\fR

.TP
\fB{on,off}\fR
enable or disable the payload power

.TP
\fBcurrent_limit\fR
current limit in amperes

.SH COMMAND \fI\,'ipmitool.py picmg send'\/\fR
usage: ipmitool.py picmg send [\-h] <command> ...

Expand Down
40 changes: 21 additions & 19 deletions pyipmi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import time
import ast
from typing import Any
from typing import Any, Literal

from . import bmc
from . import chassis
Expand Down Expand Up @@ -94,8 +94,8 @@ def __str__(self) -> str:
class Target:
"""The Target class represents an IPMI target."""

routing = None
ipmb_address = None
routing: list[Routing] | None = None
ipmb_address: int | None = None

def __init__(self, ipmb_address: int | None = None,
routing: str | list[tuple] | None = None) -> None:
Expand Down Expand Up @@ -183,14 +183,14 @@ def __init__(self, interface: Any = None, target: Target | None = None,
self.requester = requester if requester is not None else NullRequester()

for base in Ipmi.__bases__:
base.__init__(self)
base.__init__(self) # type: ignore[misc]

def __enter__(self) -> Ipmi:
self.open()
return self

def __exit__(self, exception_type: Any, exception_value: Any,
traceback: Any) -> bool:
traceback: Any) -> Literal[False]:
self.close()
return False

Expand Down Expand Up @@ -259,33 +259,35 @@ def raw_command(self, lun: int, netfn: int, raw_bytes: bytes) -> bytes:
return self.interface.send_and_receive_raw(self.target, lun, netfn,
raw_bytes)

def _get_interface(self) -> Any:
@property
def interface(self) -> Any:
try:
return self._interface
except AttributeError:
raise RuntimeError('No interface has been set') from None

def _get_session(self) -> Session:
@interface.setter
def interface(self, interface: Any) -> None:
self._interface = interface

@property
def session(self) -> Session:
try:
return self._session
except AttributeError:
raise RuntimeError('No IPMI session has been set') from None

def _get_target(self) -> Target:
@session.setter
def session(self, session: Session) -> None:
self._session = session

@property
def target(self) -> Target | None:
try:
return self._target
except AttributeError:
raise RuntimeError('No IPMI target has been set') from None

def _set_interface(self, interface: Any) -> None:
self._interface = interface

def _set_session(self, session: Session) -> None:
self._session = session

def _set_target(self, target: Target) -> None:
@target.setter
def target(self, target: Target | None) -> None:
self._target = target

target = property(_get_target, _set_target)
interface = property(_get_interface, _set_interface)
session = property(_get_session, _set_session)
3 changes: 2 additions & 1 deletion pyipmi/bmc.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
from .utils import check_completion_code
from .state import State
from .fields import VersionField
from .mixin import IpmiMixin


class Bmc:
class Bmc(IpmiMixin):
def get_device_id(self) -> DeviceId:
return DeviceId(self.send_message_with_name('GetDeviceId'))

Expand Down
25 changes: 13 additions & 12 deletions pyipmi/chassis.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .msgs import create_request_by_name, Message
from .utils import check_completion_code, check_rsp_completion_code, ByteBuffer
from .state import State
from .mixin import IpmiMixin

from .msgs.chassis import \
CONTROL_POWER_DOWN, CONTROL_POWER_UP, CONTROL_POWER_CYCLE, \
Expand Down Expand Up @@ -146,7 +147,7 @@ def boot_options_to_data(boot_device: BootDevice, boot_mode: str,
return data


class Chassis:
class Chassis(IpmiMixin):
def get_chassis_status(self) -> ChassisStatus:
return ChassisStatus(self.send_message_with_name('GetChassisStatus'))

Expand Down Expand Up @@ -227,17 +228,17 @@ def set_boot_options(self, boot_device: BootDevice, boot_mode: str,


class ChassisStatus(State):
power_on = None
overload = None
interlock = None
fault = None
control_fault = None
restore_policy = None
id_cmd_state_info_support = None
chassis_id_state = None
front_panel_button_capabilities = None
last_event = []
chassis_state = []
power_on: bool | None = None
overload: bool | None = None
interlock: bool | None = None
fault: bool | None = None
control_fault: bool | None = None
restore_policy: int | None = None
id_cmd_state_info_support: bool | None = None
chassis_id_state: int | None = None
front_panel_button_capabilities: int | None = None
last_event: list[str] = []
chassis_state: list[str] = []

def _from_response(self, rsp: Message) -> None:
# don't append to the lists shared by all instances
Expand Down
3 changes: 2 additions & 1 deletion pyipmi/dcmi.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
ENTITY_ID_DCMI_BASEBOARD)
from .msgs import Message, create_request_by_name
from .utils import check_rsp_completion_code
from .mixin import IpmiMixin


PARAM_SUPPORTED_DCMI_CAPABILITIES = 1
Expand Down Expand Up @@ -50,7 +51,7 @@
ENTITY_ID_DCMI_BASEBOARD)


class Dcmi:
class Dcmi(IpmiMixin):
def get_dcmi_capabilities(self, selector: int) -> Message:
rsp = self.send_message_with_name('GetDcmiCapabilities',
parameter_selector=selector)
Expand Down
2 changes: 1 addition & 1 deletion pyipmi/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def find_cc_desc(error_cc: int, cmdid: int | None = None,
if error_cc == cc[0]:
return cc[1]
# Then search in command specific completion codes
if cmdid is not None:
if cmdid is not None and netfn is not None:
command_cc = CC_ERR_CMD_SPECIFIC_DESC.get((netfn, cmdid, group_extension), {})
descr = command_cc.get(error_cc, "Unknown error description")
return descr
Expand Down
3 changes: 2 additions & 1 deletion pyipmi/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@

from .utils import check_completion_code
from .msgs import create_request_by_name
from .mixin import IpmiMixin

EVENT_ASSERTION = 0
EVENT_DEASSERTION = 1


class Event:
class Event(IpmiMixin):
def set_event_receiver(self, ipmb_address: int, lun: int) -> None:
req = create_request_by_name('SetEventReceiver')
req.event_receiver.ipmb_i2c_slave_address = ipmb_address
Expand Down
29 changes: 15 additions & 14 deletions pyipmi/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import array
from collections.abc import Sequence

from .errors import DecodingError
from .utils import py3_array_tobytes
Expand All @@ -18,20 +19,20 @@ class VersionField:
VERSION_FIELD_LEN = 2
VERSION_WITH_AUX_FIELD_LEN = 6

def __init__(self, data: bytes | str | None = None) -> None:
self.major = None
self.minor = None
def __init__(self, data: str | Sequence[int] | None = None) -> None:
self.major: int | None = None
self.minor: int | None = None
if data:
self._from_data(data)

def _from_data(self, data: bytes | str) -> None:
def _from_data(self, data: str | Sequence[int]) -> None:
if isinstance(data, str):
data = [ord(c) for c in data]

data = array.array('B', data)
self.version = self._decode_data(data[0:2])
if len(data) == self.VERSION_WITH_AUX_FIELD_LEN:
self.auxiliary = data[2:6]
buf = array.array('B', data)
self._decode_data(buf[0:2])
if len(buf) == self.VERSION_WITH_AUX_FIELD_LEN:
self.auxiliary = buf[2:6]

def __str__(self) -> str:
return self.version_to_string()
Expand All @@ -51,7 +52,7 @@ def version_to_string(self) -> str:
return ''.join("%s.%s" % (self.major, self.minor))


def _unpack6bitascii(data: bytes) -> str:
def _unpack6bitascii(data: Sequence[int]) -> str:
"""Unpack the 6bit ascii encoded string."""
string = ''
for i in range(0, len(data), 3):
Expand All @@ -78,7 +79,7 @@ class TypeLengthString:
TYPE_6BIT_ASCII = 2
TYPE_ASCII_OR_UTF16 = 3

def __init__(self, data: bytes | None = None, offset: int = 0,
def __init__(self, data: Sequence[int] | None = None, offset: int = 0,
force_lang_eng: bool = False, sdr: bool = False) -> None:
if data:
self._from_data(data, offset, force_lang_eng)
Expand All @@ -89,7 +90,7 @@ def __str__(self) -> str:
else:
return self.string.replace('\x00', '')

def _from_data(self, data: bytes, offset: int = 0,
def _from_data(self, data: Sequence[int], offset: int = 0,
force_lang_eng: bool = False) -> None:
self.offset = offset
self.field_type = data[offset] >> 6 & 0x3
Expand All @@ -98,7 +99,7 @@ def _from_data(self, data: bytes, offset: int = 0,
self.raw = data[offset+1:offset+1+self.length]

if self.field_type == self.TYPE_BCD_PLUS:
self.string = self.raw.decode('bcd+')
self.string = bytes(self.raw).decode('bcd+')
elif self.field_type == self.TYPE_6BIT_ASCII:
self.string = _unpack6bitascii(self.raw)
else:
Expand All @@ -108,13 +109,13 @@ def _from_data(self, data: bytes, offset: int = 0,

class FruTypeLengthString(TypeLengthString):

def __init__(self, data: bytes | None = None, offset: int = 0,
def __init__(self, data: Sequence[int] | None = None, offset: int = 0,
force_lang_eng: bool = False) -> None:
super().__init__(data, offset, force_lang_eng, sdr=False)


class SdrTypeLengthString(TypeLengthString):

def __init__(self, data: bytes | None = None, offset: int = 0,
def __init__(self, data: Sequence[int] | None = None, offset: int = 0,
force_lang_eng: bool = False) -> None:
super().__init__(data, sdr=True)
Loading
Loading