# ******************************************************
#
# Copyright (c) Extreme Networks Inc. 2014, 2021
# All rights reserved
#
# ******************************************************
import _exos_ext_dm as ext
import logging
import functools
from exos.api.util import Observable, NotifyBase, Enum
try:
import cPickle as pickle
except ImportError:
import pickle
# Bring the platform_id stuff up. This isn't public (yet).
from _exos_ext_dm import ( # noqa: F401
get_platform_id,
HAL_PLATFORM_ID_PC_STACKABLE,
HAL_PLATFORM_ID_PC_CHASSIS,
HAL_PLATFORM_ID_PC_STACKABLE_STACK,
HAL_PLATFORM_ID_EXTENT,
# Process names
THTTPD_PROCESS_NAME,
FDB_PROCESS_NAME,
OTM_PROCESS_NAME,
VLAN_PROCESS_NAME,
)
logger = logging.getLogger("pyapi")
# Message IDs we'll use while checkpointing.
DM_CHKPT_PARTIAL = 0x1000
#: DM Process states.
ProcessState = Enum(
[
"UNKNOWN",
"FAIL",
"STOPPED",
"STOPPING",
"STARTED",
"BOOTING",
"CONNECT",
"DBSYNC",
"LOADCFG",
"READY",
"DELETED",
]
)
ProcessState._mapping = {
ext.DM_PROCESS_STATE_FAIL: ProcessState.FAIL,
ext.DM_PROCESS_STATE_STOPPED: ProcessState.STOPPED,
ext.DM_PROCESS_STATE_STOPPING: ProcessState.STOPPING,
ext.DM_PROCESS_STATE_STARTED: ProcessState.STARTED,
ext.DM_PROCESS_STATE_BOOTING: ProcessState.BOOTING,
ext.DM_PROCESS_STATE_CONNECT: ProcessState.CONNECT,
ext.DM_PROCESS_STATE_DBSYNC: ProcessState.DBSYNC,
ext.DM_PROCESS_STATE_LOADCFG: ProcessState.LOADCFG,
ext.DM_PROCESS_STATE_READY: ProcessState.READY,
ext.DM_PROCESS_STATE_DELETED: ProcessState.DELETED,
}
#: DM Node states.
NodeState = Enum(
[
"UNKNOWN",
"PRIMARY",
"BACKUP",
"STANDBY",
]
)
NodeState._mapping = {
ext.DM_NODE_STATE_PRIMARY: NodeState.PRIMARY,
ext.DM_NODE_STATE_BACKUP: NodeState.BACKUP,
ext.DM_NODE_STATE_STANDBY: NodeState.STANDBY,
}
class DMNotify(NotifyBase):
"""Notification class for DM."""
@classmethod
def _map_process_state(cls, state):
return ProcessState.map_enum(state, ProcessState.UNKNOWN)
@classmethod
def _map_node_state(cls, state):
return NodeState.map_enum(state, NodeState.UNKNOWN)
@Observable()
def card_up(self, **kwds):
"""A card was joined the switch. The card is identified by
*card_id*."""
return kwds
@Observable()
def card_down(self, **kwds):
"""A card was left the switch. The card is identified by
*card_id*."""
return kwds
@Observable()
def process_state_update(self, **kwds):
"""The state of an EXOS process *process_name* has changed to *state*. State is a
value of :class:ProcessState."""
kwds["state"] = self._map_process_state(kwds["state"])
return kwds
@Observable()
def node_state_update(self, **kwds):
"""This node has been instructed to change to *state*."""
kwds["state"] = self._map_node_state(kwds["state"])
return kwds
@Observable()
def checkpointing_update(self, **kwds):
"""The *state* of checkpointing has changed. If state is True,
the process is expected to be syncing with its backup. If False,
it should not."""
kwds["state"] = bool(kwds["state"])
return kwds
@Observable()
def config_update(self, **kwds):
"""The *sys_name* specifies the new device name of the switch"""
return kwds
#: Collection of DM Observables.
notify = DMNotify()
# Tell our extension to send us notifications. This doesn't happen in an init
# because DM is always ready.
ext.set_notify_callbacks(notify)
[docs]def is_capability_supported(capability):
"""Return ``True`` if this switch supports the given *capability*, otherwise
``False``. *capability* is a string. If it is not a recognized,
a :class:`ValueError` is raised."""
return True if ext.is_capability_supported(capability) else False
def is_chassis():
"""Return True if we are running on a chassis."""
return ext.get_platform_type() == ext.PLATFORM_TYPE_CHASSIS
[docs]def is_stackable():
"""Return ``True`` if we are running on a stackable. However, stacking
may not be enabled."""
return ext.get_platform_type() == ext.PLATFORM_TYPE_STACKABLE
[docs]def is_primary():
"""Return ``True`` if this switch is the primary."""
return True if ext.am_i_primary() else False
[docs]def is_backup():
"""Return ``True`` if this switch is the backup."""
return True if ext.am_i_backup() else False
[docs]def is_standby():
"""Return ``True`` if this switch is a standby."""
return True if ext.am_i_standby() else False
[docs]def is_checkpointing():
"""Return ``True`` if this switch is ready to checkpoint data."""
return True if ext.is_checkpointing_on() else False
[docs]class SlotProperties(object):
@property
def self(self):
"""The current switch's slot number."""
return ext.get_slot_self()
@property
def first(self):
"""The first valid slot number."""
return ext.get_slot_first()
@property
def last(self):
"""The last valid slot number."""
return ext.get_slot_last()
@property
def first_io(self):
return ext.get_slot_first_io()
@property
def last_io(self):
return ext.get_slot_last_io()
#: Collection of slot number properties. This is a singleton instance of
#: :class:`SlotProperties`.
slot = SlotProperties()
[docs]def get_sysname():
"""Return the switch name"""
return ext.get_sysname()
[docs]def get_process_state(process_name):
"""Get the `ProcessState` of *process_name*."""
return ProcessState.map_enum(ext.get_process_state(process_name), ProcessState.UNKNOWN)
def set_process_state(process_name, process_state):
"""Set the *process_state" of *process_name*."""
dm_process_state = ProcessState.map_val(process_state, ext.DM_PROCESS_STATE_UNKNOWN)
return ProcessState.map_enum(ext.set_process_state(process_name, dm_process_state), ProcessState.UNKNOWN)
def _call_on(callfn, fn, *args, **kwds):
if not callable(fn):
raise TypeError("Function is not callable.")
# There's some inconsistency between exceptions from pickle and cPickle.
# Hack around it here with a try/except.
# http://bugs.python.org/issue1457119
try:
msg = pickle.dumps(functools.partial(fn, *args, **kwds))
except TypeError as e:
raise pickle.PicklingError(str(e))
return True if callfn(DM_CHKPT_PARTIAL, msg) == 0 else False
[docs]def call_on_primary(fn, *args, **kwds):
"""Call *fn* on the primary with *args* and *kwds*. *fn*, *args*, *kwds*
must be pickle-able. If not, a :class:`PicklingError` is raised. ``True``
is returned if the message was sent.
"""
return _call_on(ext.write_primary, fn, *args, **kwds)
[docs]def call_on_backup(fn, *args, **kwds):
"""Call *fn* on the backup with *args* and *kwds*. *fn*, *args*, *kwds*
must be pickle-able. If not, a :class:`PicklingError` is raised. ``True``
is returned if the message was sent.
"""
return _call_on(ext.write_backup, fn, *args, **kwds)
[docs]def call_on_standby(slot, fn, *args, **kwds):
"""Call *fn* on the standby with *args* and *kwds*. *fn*, *args*, *kwds*
must be pickle-able. If not, a :class:`PicklingError` is raised. ``True``
is returned if the message was sent.
"""
# Closure to hold slot
def call_on_standby_slot(fn, *args, **kwds):
return ext.write_standby(slot, fn, *args, **kwds)
return _call_on(call_on_standby_slot, fn, *args, **kwds)
[docs]def call_on_standbys(fn, *args, **kwds):
"""Call *fn* on all standbys with *args* and *kwds*. *fn*, *args*, *kwds*
must be pickle-able. If not, a :class:`PicklingError` is raised. ``True``
is returned if the message was sent.
"""
return _call_on(ext.bcast_standby, fn, *args, **kwds)
def _chkpt_callback(msg_id, msg):
"""Private function to handle checkpoint messages. We'll try to de-pickle them
and call the result.
"""
if msg_id == DM_CHKPT_PARTIAL:
try:
fn = pickle.loads(msg)
except Exception:
logger.exception("Unable to load checkpoint message")
try:
fn()
except Exception:
logger.exception("Failure in checkpoint callback")
else:
logger.error("Unknown checkpoint msg_id %d", msg_id)
# Set the checkpointing callback to our private function above. This allows
# call_on_backup(), etc. to magically work.
ext.set_checkpoint_callback(_chkpt_callback)