summaryrefslogtreecommitdiff
path: root/cxmanage_api/node.py
diff options
context:
space:
mode:
Diffstat (limited to 'cxmanage_api/node.py')
-rw-r--r--cxmanage_api/node.py881
1 files changed, 604 insertions, 277 deletions
diff --git a/cxmanage_api/node.py b/cxmanage_api/node.py
index 9ccae97..133ebb1 100644
--- a/cxmanage_api/node.py
+++ b/cxmanage_api/node.py
@@ -1,4 +1,8 @@
-# Copyright (c) 2012, Calxeda Inc.
+# pylint: disable=C0302
+"""Calxeda: node.py"""
+
+
+# Copyright (c) 2012-2013, Calxeda Inc.
#
# All rights reserved.
#
@@ -28,28 +32,31 @@
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.
-
import os
import re
-import subprocess
import time
+import tempfile
+import socket
+import subprocess
from pkg_resources import parse_version
from pyipmi import make_bmc, IpmiError
from pyipmi.bmc import LanBMC as BMC
from tftpy.TftpShared import TftpException
+from cxmanage_api import loggers
from cxmanage_api import temp_file
from cxmanage_api.tftp import InternalTftp, ExternalTftp
from cxmanage_api.image import Image as IMAGE
from cxmanage_api.ubootenv import UbootEnv as UBOOTENV
from cxmanage_api.ip_retriever import IPRetriever as IPRETRIEVER
from cxmanage_api.cx_exceptions import TimeoutError, NoSensorError, \
- NoFirmwareInfoError, SocmanVersionError, FirmwareConfigError, \
- PriorityIncrementError, NoPartitionError, TransferFailure, \
- ImageSizeError, PartitionInUseError
+ SocmanVersionError, FirmwareConfigError, PriorityIncrementError, \
+ NoPartitionError, TransferFailure, ImageSizeError, \
+ PartitionInUseError, UbootenvError, EEPROMUpdateError, ParseError
+# pylint: disable=R0902, R0904
class Node(object):
"""A node is a single instance of an ECME.
@@ -75,13 +82,13 @@ class Node(object):
:type ubootenv: `UbootEnv <ubootenv.html>`_
"""
-
+ # pylint: disable=R0913
def __init__(self, ip_address, username="admin", password="admin",
tftp=None, ecme_tftp_port=5001, verbose=False, bmc=None,
image=None, ubootenv=None, ipretriever=None):
"""Default constructor for the Node class."""
if (not tftp):
- tftp = InternalTftp()
+ tftp = InternalTftp.default()
# Dependency Integration
if (not bmc):
@@ -115,7 +122,7 @@ class Node(object):
return hash(self.ip_address)
def __str__(self):
- return 'Node: %s' % self.ip_address
+ return 'Node %i (%s)' % (self.node_id, self.ip_address)
@property
def tftp_address(self):
@@ -183,7 +190,8 @@ class Node(object):
:param macaddr: MAC address to add
:type macaddr: string
- :raises IpmiError: If errors in the command occur with BMC communication.
+ :raises IpmiError: If errors in the command occur with BMC \
+communication.
"""
self.bmc.fabric_add_macaddr(iface=iface, macaddr=macaddr)
@@ -198,7 +206,8 @@ class Node(object):
:param macaddr: MAC address to remove
:type macaddr: string
- :raises IpmiError: If errors in the command occur with BMC communication.
+ :raises IpmiError: If errors in the command occur with BMC \
+communication.
"""
self.bmc.fabric_rm_macaddr(iface=iface, macaddr=macaddr)
@@ -217,12 +226,9 @@ class Node(object):
:rtype: boolean
"""
- try:
- return self.bmc.get_chassis_status().power_on
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ return self.bmc.get_chassis_status().power_on
- def set_power(self, mode):
+ def set_power(self, mode, ignore_existing_state=False):
"""Send an IPMI power command to this target.
>>> # To turn the power 'off'
@@ -236,12 +242,18 @@ class Node(object):
:param mode: Mode to set the power state to. ('on'/'off')
:type mode: string
+ :param ignore_existing_state: Flag that allows the caller to only try
+ to turn on or off the node if it is not
+ turned on or off, respectively.
+ :type ignore_existing_state: boolean
"""
- try:
- self.bmc.set_chassis_power(mode=mode)
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ if ignore_existing_state:
+ if self.get_power() and mode == "on":
+ return
+ if not self.get_power() and mode == "off":
+ return
+ self.bmc.set_chassis_power(mode=mode)
def get_power_policy(self):
"""Return power status reported by IPMI.
@@ -252,13 +264,11 @@ class Node(object):
:return: The Nodes current power policy.
:rtype: string
- :raises IpmiError: If errors in the command occur with BMC communication.
+ :raises IpmiError: If errors in the command occur with BMC \
+communication.
"""
- try:
- return self.bmc.get_chassis_status().power_restore_policy
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ return self.bmc.get_chassis_status().power_restore_policy
def set_power_policy(self, state):
"""Set default power state for Linux side.
@@ -273,10 +283,7 @@ class Node(object):
:type state: string
"""
- try:
- self.bmc.set_chassis_policy(state)
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ self.bmc.set_chassis_policy(state)
def mc_reset(self, wait=False):
"""Sends a Master Control reset command to the node.
@@ -290,12 +297,7 @@ class Node(object):
:raises IPMIError: If there is an IPMI error communicating with the BMC.
"""
- try:
- result = self.bmc.mc_reset("cold")
- if (hasattr(result, "error")):
- raise Exception(result.error)
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ self.bmc.mc_reset("cold")
if wait:
deadline = time.time() + 300.0
@@ -314,6 +316,29 @@ class Node(object):
else:
raise Exception("Reset timed out")
+ def get_sel(self):
+ """Get the system event log for this node.
+
+ >>> node.get_sel()
+ ['1 | 06/21/2013 | 16:13:31 | System Event #0xf4 |',
+ '0 | 06/27/2013 | 20:25:18 | System Boot Initiated #0xf1 | \
+Initiated by power up | Asserted',
+ '1 | 06/27/2013 | 20:25:35 | Watchdog 2 #0xfd | Hard reset | \
+Asserted',
+ '2 | 06/27/2013 | 20:25:18 | System Boot Initiated #0xf1 | \
+Initiated by power up | Asserted',
+ '3 | 06/27/2013 | 21:01:13 | System Event #0xf4 |',
+ ...
+ ]
+ >>> #
+ >>> # Output trimmed for brevity
+ >>> #
+
+ :returns: The node's system event log
+ :rtype: string
+ """
+ return self.bmc.sel_elist()
+
def get_sensors(self, search=""):
"""Get a list of sensor objects that match search criteria.
@@ -357,11 +382,8 @@ class Node(object):
:rtype: dictionary of pyipmi objects
"""
- try:
- sensors = [x for x in self.bmc.sdr_list()
- if search.lower() in x.sensor_name.lower()]
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ sensors = [x for x in self.bmc.sdr_list()
+ if search.lower() in x.sensor_name.lower()]
if (len(sensors) == 0):
if (search == ""):
@@ -515,34 +537,19 @@ class Node(object):
:return: Returns a list of FWInfo objects for each
:rtype: list
- :raises NoFirmwareInfoError: If no fw info exists for any partition.
- :raises IpmiError: If errors in the command occur with BMC communication.
+ :raises IpmiError: If errors in the command occur with BMC \
+communication.
"""
- try:
- fwinfo = [x for x in self.bmc.get_firmware_info()
- if hasattr(x, "partition")]
- if (len(fwinfo) == 0):
- raise NoFirmwareInfoError("Failed to retrieve firmware info")
-
- # Clean up the fwinfo results
- for entry in fwinfo:
- if (entry.version == ""):
- entry.version = "Unknown"
-
- # Flag CDB as "in use" based on socman info
- for a in range(1, len(fwinfo)):
- previous = fwinfo[a - 1]
- current = fwinfo[a]
- if (current.type.split()[1][1:-1] == "CDB" and
- current.in_use == "Unknown"):
- if (previous.type.split()[1][1:-1] != "SOC_ELF"):
- current.in_use = "1"
- else:
- current.in_use = previous.in_use
- return fwinfo
- except IpmiError as error_details:
- raise IpmiError(self._parse_ipmierror(error_details))
+ fwinfo = [x for x in self.bmc.get_firmware_info()
+ if hasattr(x, "partition")]
+
+ # Clean up the fwinfo results
+ for entry in fwinfo:
+ if (entry.version == ""):
+ entry.version = "Unknown"
+
+ return fwinfo
def get_firmware_info_dict(self):
"""Gets firmware info for each partition on the Node.
@@ -581,8 +588,8 @@ class Node(object):
:return: Returns a list of FWInfo objects for each
:rtype: list
- :raises NoFirmwareInfoError: If no fw info exists for any partition.
- :raises IpmiError: If errors in the command occur with BMC communication.
+ :raises IpmiError: If errors in the command occur with BMC \
+communication.
"""
return [vars(info) for info in self.get_firmware_info()]
@@ -604,10 +611,12 @@ class Node(object):
try:
self._check_firmware(package, partition_arg, priority)
return True
- except (SocmanVersionError, FirmwareConfigError, PriorityIncrementError,
- NoPartitionError, ImageSizeError, PartitionInUseError):
+ except (SocmanVersionError, FirmwareConfigError,
+ PriorityIncrementError, NoPartitionError, ImageSizeError,
+ PartitionInUseError):
return False
+ # pylint: disable=R0914, R0912, R0915
def update_firmware(self, package, partition_arg="INACTIVE",
priority=None):
""" Update firmware on this target.
@@ -627,45 +636,143 @@ class Node(object):
changed.
"""
+
+ new_directory = "~/.cxmanage/logs/%s" % self.ip_address
+ new_directory = os.path.expanduser(new_directory)
+ if not os.path.exists(new_directory):
+ os.makedirs(new_directory)
+
+ timestamp = time.strftime("%Y%m%d%H%M%S")
+ new_filename = "%s-fwupdate.log" % timestamp
+ new_filepath = os.path.join(new_directory, new_filename)
+
+ logger = loggers.FileLogger(new_filepath)
+
+ logger.info(
+ "Firmware Update Log for Node %d" % self.node_id
+ )
+ logger.info("ECME IP address: " + self.ip_address)
+
+ version_info = self.get_versions()
+ logger.info(
+ "\nOld firmware version: " + \
+ version_info.firmware_version)
+
+ if package.version:
+ logger.info("New firmware version: " + package.version)
+ else:
+ logger.warn("New firmware version name unavailable.")
+
+ logger.info(
+ "\n[ Pre-Update Firmware Info for Node %d ]" %
+ self.node_id
+ )
+
fwinfo = self.get_firmware_info()
+ num_ubootenv_partitions = len([x for x in fwinfo
+ if "UBOOTENV" in x.type])
+
+ for partition in fwinfo:
+ logger.info("\nPartition : %s" % partition.partition)
+ info_string = "Type : %s" % partition.type + \
+ "\nOffset : %s" % partition.offset + \
+ "\nSize : %s" % partition.size + \
+ "\nPriority : %s" % partition.priority + \
+ "\nDaddr : %s" % partition.daddr + \
+ "\nFlags : %s" % partition.flags + \
+ "\nVersion : %s" % partition.version + \
+ "\nIn Use : %s" % partition.in_use
+ logger.info(info_string)
# Get the new priority
if (priority == None):
priority = self._get_next_priority(fwinfo, package)
+ logger.info(
+ "\nPriority: " + str(priority)
+ )
+
+ images_to_upload = len(package.images)
+ logger.info(
+ "package.images: Images to upload: %d" % images_to_upload
+ )
+
updated_partitions = []
+ image_uploading = 1
for image in package.images:
- if (image.type == "UBOOTENV"):
+ logger.info(
+ "\nUploading image %d of %d" %
+ (image_uploading, images_to_upload)
+ )
+
+ if image.type == "UBOOTENV" and num_ubootenv_partitions >= 2:
+ logger.info(
+ "Trying ubootenv for image %d..." % image_uploading
+ )
+
# Get partitions
running_part = self._get_partition(fwinfo, image.type, "FIRST")
factory_part = self._get_partition(fwinfo, image.type,
"SECOND")
+ # Extra \n's here for ease of reading output
+ logger.info(
+ "\nFirst ('FIRST') partition:\n" + \
+ str(running_part) + \
+ "\n\nSecond ('FACTORY') partition:\n" + \
+ str(factory_part)
+ )
+
# Update factory ubootenv
self._upload_image(image, factory_part, priority)
+ # Extra \n for output formatting
+ logger.info(
+ "\nDone uploading factory image"
+ )
+
# Update running ubootenv
old_ubootenv_image = self._download_image(running_part)
old_ubootenv = self.ubootenv(open(
old_ubootenv_image.filename).read())
+
+ logger.info(
+ "Done getting old ubootenv image"
+ )
+
try:
ubootenv = self.ubootenv(open(image.filename).read())
ubootenv.set_boot_order(old_ubootenv.get_boot_order())
+ ubootenv.set_pxe_interface(old_ubootenv.get_pxe_interface())
+
+ logger.info(
+ "Set boot order to %s" % old_ubootenv.get_boot_order()
+ )
filename = temp_file()
- with open(filename, "w") as f:
- f.write(ubootenv.get_contents())
+ with open(filename, "w") as file_:
+ file_.write(ubootenv.get_contents())
+
ubootenv_image = self.image(filename, image.type, False,
image.daddr, image.skip_crc32,
image.version)
self._upload_image(ubootenv_image, running_part,
priority)
- except (ValueError, Exception):
+
+ logger.info(
+ "Done uploading ubootenv image to first " + \
+ "partition ('running partition')"
+ )
+ except (ValueError, UbootenvError):
self._upload_image(image, running_part, priority)
updated_partitions += [running_part, factory_part]
else:
+ logger.info(
+ "Using Non-ubootenv for image %d..." %
+ image_uploading
+ )
# Get the partitions
if (partition_arg == "BOTH"):
partitions = [self._get_partition(fwinfo, image.type,
@@ -681,9 +788,17 @@ class Node(object):
updated_partitions += partitions
+ logger.info(
+ "Done uploading image %d of %d" %
+ (image_uploading, images_to_upload)
+ )
+ image_uploading = image_uploading + 1
+
if package.version:
self.bmc.set_firmware_version(package.version)
+ logger.info("") # For readability
+
# Post verify
fwinfo = self.get_firmware_info()
for old_partition in updated_partitions:
@@ -691,48 +806,138 @@ class Node(object):
new_partition = fwinfo[partition_id]
if new_partition.type != old_partition.type:
+ logger.error(
+ "Update failed (partition %i, type changed)"
+ % partition_id
+ )
raise Exception("Update failed (partition %i, type changed)"
% partition_id)
if int(new_partition.priority, 16) != priority:
+ logger.error(
+ "Update failed (partition %i, wrong priority)"
+ % partition_id
+ )
raise Exception("Update failed (partition %i, wrong priority)"
% partition_id)
if int(new_partition.flags, 16) & 2 != 0:
+ logger.error(
+ "Update failed (partition %i, not activated)"
+ % partition_id
+ )
raise Exception("Update failed (partition %i, not activated)"
% partition_id)
- result = self.bmc.check_firmware(partition_id)
- if not hasattr(result, "crc32") or result.error != None:
- raise Exception("Update failed (partition %i, post-crc32 fail)"
- % partition_id)
+ self.bmc.check_firmware(partition_id)
+ logger.info(
+ "Check complete for partition %d" % partition_id
+ )
+
+ logger.info(
+ "\nDone updating firmware."
+ )
+
+ print("\nLog saved to " + new_filepath)
+
+ def update_node_eeprom(self, image):
+ """Updates the node EEPROM
+
+ .. note::
+ A power cycle is required for the update to take effect
+
+ >>> node.update_node_eeprom('builds/dual_node_0_v3.0.0.img')
+
+ :param image: The location of an EEPROM image
+ :type image: string
+ :raises EEPROMUpdateError: When an error is encountered while \
+updating the EEPROM
+
+ """
+ # Does the image exist?
+ if(not os.path.exists(image)):
+ raise EEPROMUpdateError(
+ '%s does not exist' % image
+ )
+ node_hw_ver = self.get_versions().hardware_version
+ # Is this configuration valid for EEPROM updates?
+ if('Dual Node' not in node_hw_ver):
+ raise EEPROMUpdateError(
+ 'eepromupdate is only valid on TerraNova systems'
+ )
+ # Is this image valid?
+ if('Uplink' in node_hw_ver):
+ image_prefix = 'dual_uplink_node_%s' % (self.node_id % 4)
+ else:
+ image_prefix = 'dual_node_%s' % (self.node_id % 4)
+ if(image_prefix not in image):
+ raise EEPROMUpdateError(
+ '%s is not a valid node EEPROM image for this node' % image
+ )
+ # Perform the upgrade
+ ipmi_command = 'fru write 81 %s' % image
+ self.ipmitool_command(ipmi_command.split(' '))
+
+ def update_slot_eeprom(self, image):
+ """Updates the slot EEPROM
+
+ .. note::
+ A power cycle is required for the update to take effect
+
+ >>> node.update_slot_eeprom('builds/tn_storage.single_slot_v3.0.0.img')
+
+ :param image: The location of an EEPROM image
+ :type image: string
+
+ :raises EEPROMUpdateError: When an error is encountered while \
+updating the EEPROM
+
+ """
+ # Does the image exist?
+ if(not os.path.exists(image)):
+ raise EEPROMUpdateError(
+ '%s does not exist' % image
+ )
+ node_hw_ver = self.get_versions().hardware_version
+ # Is this configuration valid for EEPROM updates?
+ if('Dual Node' not in node_hw_ver):
+ raise EEPROMUpdateError(
+ 'eepromupdate is only valid on TerraNova systems'
+ )
+ # Is this image valid?
+ if('tn_storage.single_slot' not in image):
+ raise EEPROMUpdateError(
+ '%s is an invalid image for slot EEPROM' % image
+ )
+ # Perform the upgrade
+ ipmi_command = 'fru write 82 %s' % image
+ self.ipmitool_command(ipmi_command.split(' '))
def config_reset(self):
"""Resets configuration to factory defaults.
>>> node.config_reset()
- :raises IpmiError: If errors in the command occur with BMC communication.
- :raises Exception: If there are errors within the command response.
+ :raises IpmiError: If errors in the command occur with BMC \
+communication.
"""
- try:
- # Reset CDB
- result = self.bmc.reset_firmware()
- if (hasattr(result, "error")):
- raise Exception(result.error)
+ # Reset CDB
+ self.bmc.reset_firmware()
- # Reset ubootenv
- fwinfo = self.get_firmware_info()
+ # Reset ubootenv
+ fwinfo = self.get_firmware_info()
+ try:
running_part = self._get_partition(fwinfo, "UBOOTENV", "FIRST")
factory_part = self._get_partition(fwinfo, "UBOOTENV", "SECOND")
image = self._download_image(factory_part)
self._upload_image(image, running_part)
- # Clear SEL
- self.bmc.sel_clear()
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ except NoPartitionError:
+ pass # Only one partition? Don't mess with it!
+
+ # Clear SEL
+ self.bmc.sel_clear()
def set_boot_order(self, boot_args):
"""Sets boot-able device order for this node.
@@ -754,8 +959,8 @@ class Node(object):
priority = max(int(x.priority, 16) for x in [first_part, active_part])
filename = temp_file()
- with open(filename, "w") as f:
- f.write(ubootenv.get_contents())
+ with open(filename, "w") as file_:
+ file_.write(ubootenv.get_contents())
ubootenv_image = self.image(filename, image.type, False, image.daddr,
image.skip_crc32, image.version)
@@ -770,6 +975,41 @@ class Node(object):
"""
return self.get_ubootenv().get_boot_order()
+ def set_pxe_interface(self, interface):
+ """Sets pxe interface for this node.
+
+ >>> node.set_boot_order('eth0')
+
+ :param interface: Interface pass on to the uboot environment.
+ :type boot_args: string
+
+ """
+ fwinfo = self.get_firmware_info()
+ first_part = self._get_partition(fwinfo, "UBOOTENV", "FIRST")
+ active_part = self._get_partition(fwinfo, "UBOOTENV", "ACTIVE")
+
+ # Download active ubootenv, modify, then upload to first partition
+ image = self._download_image(active_part)
+ ubootenv = self.ubootenv(open(image.filename).read())
+ ubootenv.set_pxe_interface(interface)
+ priority = max(int(x.priority, 16) for x in [first_part, active_part])
+
+ filename = temp_file()
+ with open(filename, "w") as file_:
+ file_.write(ubootenv.get_contents())
+
+ ubootenv_image = self.image(filename, image.type, False, image.daddr,
+ image.skip_crc32, image.version)
+ self._upload_image(ubootenv_image, first_part, priority)
+
+ def get_pxe_interface(self):
+ """Returns the current pxe interface for this node.
+
+ >>> node.get_pxe_interface()
+ 'eth0'
+ """
+ return self.get_ubootenv().get_pxe_interface()
+
def get_versions(self):
"""Get version info from this node.
@@ -784,39 +1024,59 @@ class Node(object):
:returns: The results of IPMI info basic command.
:rtype: pyipmi.info.InfoBasicResult
- :raises IpmiError: If errors in the command occur with BMC communication.
+ :raises IpmiError: If errors in the command occur with BMC \
+communication.
:raises Exception: If there are errors within the command response.
"""
- try:
- result = self.bmc.get_info_basic()
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
-
+ result = self.bmc.get_info_basic()
fwinfo = self.get_firmware_info()
- components = [("cdb_version", "CDB"),
- ("stage2_version", "S2_ELF"),
- ("bootlog_version", "BOOT_LOG"),
- ("a9boot_version", "A9_EXEC"),
- ("uboot_version", "A9_UBOOT"),
- ("ubootenv_version", "UBOOTENV"),
- ("dtb_version", "DTB")]
+
+ # components maps variables to firmware partition types
+ components = [
+ ("cdb_version", "CDB"),
+ ("stage2_version", "S2_ELF"),
+ ("bootlog_version", "BOOT_LOG"),
+ ("uboot_version", "A9_UBOOT"),
+ ("ubootenv_version", "UBOOTENV"),
+ ("dtb_version", "DTB")
+ ]
+
+ # Use firmware version to determine the chip type and name
+ # In the future, we may want to determine the chip name some other way
+ if result.firmware_version.startswith("ECX-1000"):
+ result.chip_name = "Highbank"
+ components.append(("a9boot_version", "A9_EXEC"))
+ elif result.firmware_version.startswith("ECX-2000"):
+ result.chip_name = "Midway"
+ components.append(("a15boot_version", "A9_EXEC"))
+ else:
+ result.chip_name = "Unknown"
+ components.append(("a9boot_version", "A9_EXEC"))
+ setattr(result, "chip_name", "Unknown")
+
for var, ptype in components:
try:
partition = self._get_partition(fwinfo, ptype, "ACTIVE")
setattr(result, var, partition.version)
except NoPartitionError:
pass
+
try:
card = self.bmc.get_info_card()
- setattr(result, "hardware_version", "%s X%02i" %
- (card.type, int(card.revision)))
- except IpmiError as err:
- if (self.verbose):
- print str(err)
+ result.hardware_version = "%s X%02i" % (
+ card.type, int(card.revision)
+ )
+ except IpmiError:
# Should raise an error, but we want to allow the command
# to continue gracefully if the ECME is out of date.
- setattr(result, "hardware_version", "Unknown")
+ result.hardware_version = "Unknown"
+
+ try:
+ result.pmic_version = self.bmc.pmic_get_version()
+ except IpmiError:
+ pass
+
return result
def get_versions_dict(self):
@@ -846,7 +1106,8 @@ class Node(object):
:returns: The results of IPMI info basic command.
:rtype: dictionary
- :raises IpmiError: If errors in the command occur with BMC communication.
+ :raises IpmiError: If errors in the command occur with BMC \
+communication.
:raises Exception: If there are errors within the command response.
"""
@@ -863,6 +1124,8 @@ class Node(object):
:param ipmitool_args: Arguments to pass to the ipmitool.
:type ipmitool_args: list
+ :raises IpmiError: If the IPMI command fails.
+
"""
if ("IPMITOOL_PATH" in os.environ):
command = [os.environ["IPMITOOL_PATH"]]
@@ -879,6 +1142,8 @@ class Node(object):
process = subprocess.Popen(command, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
+ if(process.returncode != 0):
+ raise IpmiError(stderr.strip())
return (stdout + stderr).strip()
def get_ubootenv(self):
@@ -907,72 +1172,93 @@ class Node(object):
:raises IpmiError: If the IPMI command fails.
:raises TftpException: If the TFTP transfer fails.
+ :raises ParseError: If we fail to parse IP info
"""
- try:
- filename = self._run_fabric_command(
- function_name='fabric_config_get_ip_info',
- )
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
-
- # Parse addresses from ipinfo file
- results = {}
- for line in open(filename):
- if (line.startswith("Node")):
- elements = line.split()
- node_id = int(elements[1].rstrip(":"))
- node_ip_address = elements[2]
-
- # Old boards used to return 0.0.0.0 sometimes -- might not be
- # an issue anymore.
- if (node_ip_address != "0.0.0.0"):
- results[node_id] = node_ip_address
+ for _ in range(3):
+ try:
+ filename = self._run_fabric_command(
+ function_name='fabric_config_get_ip_info'
+ )
- # Make sure we found something
- if (not results):
- raise TftpException("Node failed to reach TFTP server")
+ results = {}
+ for line in open(filename):
+ if line.strip():
+ elements = line.split()
+ node_id = int(elements[1].rstrip(":"))
+ ip_address = elements[2]
+ socket.inet_aton(ip_address) # IP validity check
+ results[node_id] = ip_address
+ return results
+ except (IndexError, ValueError, socket.error):
+ pass
- return results
+ raise ParseError(
+ "Failed to parse fabric IP info\n%s" % open(filename).read()
+ )
def get_fabric_macaddrs(self):
"""Gets what macaddr information THIS node knows about the Fabric.
+ >>> node.get_fabric_macaddrs()
+ {0: {0: ['fc:2f:40:ab:cd:cc'],
+ 1: ['fc:2f:40:ab:cd:cd'],
+ 2: ['fc:2f:40:ab:cd:ce']},
+ 1: {0: ['fc:2f:40:3e:66:e0'],
+ 1: ['fc:2f:40:3e:66:e1'],
+ 2: ['fc:2f:40:3e:66:e2']},
+ 2: {0: ['fc:2f:40:fd:37:34'],
+ 1: ['fc:2f:40:fd:37:35'],
+ 2: ['fc:2f:40:fd:37:36']},
+ 3: {0: ['fc:2f:40:0e:4a:74'],
+ 1: ['fc:2f:40:0e:4a:75'],
+ 2: ['fc:2f:40:0e:4a:76']}}
+
:return: Returns a map of node_ids->ports->mac_addresses.
:rtype: dictionary
:raises IpmiError: If the IPMI command fails.
:raises TftpException: If the TFTP transfer fails.
+ :raises ParseError: If we fail to parse macaddrs output
"""
- try:
- filename = self._run_fabric_command(
- function_name='fabric_config_get_mac_addresses'
- )
-
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
-
- # Parse addresses from ipinfo file
- results = {}
- for line in open(filename):
- if (line.startswith("Node")):
- elements = line.split()
- node_id = int(elements[1].rstrip(","))
- port = int(elements[3].rstrip(":"))
- mac_address = elements[4]
-
- if not node_id in results:
- results[node_id] = {}
- if not port in results[node_id]:
- results[node_id][port] = []
- results[node_id][port].append(mac_address)
+ for _ in range(3):
+ try:
+ filename = self._run_fabric_command(
+ function_name='fabric_config_get_mac_addresses'
+ )
- # Make sure we found something
- if (not results):
- raise TftpException("Node failed to reach TFTP server")
+ results = {}
+ for line in open(filename):
+ if (line.startswith("Node")):
+ elements = line.split()
+ node_id = int(elements[1].rstrip(","))
+ port = int(elements[3].rstrip(":"))
+ mac_address = elements[4]
+
+ # MAC address validity check
+ octets = [int(x, 16) for x in mac_address.split(":")]
+ if len(octets) != 6:
+ raise ParseError(
+ "Invalid MAC address: %s" % mac_address
+ )
+ elif not all(x <= 255 and x >= 0 for x in octets):
+ raise ParseError(
+ "Invalid MAC address: %s" % mac_address
+ )
+
+ if not node_id in results:
+ results[node_id] = {}
+ if not port in results[node_id]:
+ results[node_id][port] = []
+ results[node_id][port].append(mac_address)
+ return results
+ except (ValueError, IndexError, ParseError):
+ pass
- return results
+ raise ParseError(
+ "Failed to parse MAC addresses\n%s" % open(filename).read()
+ )
def get_fabric_uplink_info(self):
"""Gets what uplink information THIS node knows about the Fabric.
@@ -1001,20 +1287,37 @@ class Node(object):
node_id = int(line.replace('Node ', '')[0])
ul_info = line.replace('Node %s:' % node_id, '').strip().split(',')
node_data = {}
- for ul in ul_info:
- data = tuple(ul.split())
+ for ul_ in ul_info:
+ data = tuple(ul_.split())
node_data[data[0]] = int(data[1])
results[node_id] = node_data
- # Make sure we found something
- if (not results):
- raise TftpException("Node failed to reach TFTP server")
-
return results
def get_link_stats(self, link=0):
"""Gets the linkstats for the link specified.
+ >>> node.get_link_stats()
+ {'FS_LC0_BYTE_CNT_0': '0x0',
+ 'FS_LC0_BYTE_CNT_1': '0x0',
+ 'FS_LC0_CFG_0': '0x1000d07f',
+ 'FS_LC0_CFG_1': '0x105f',
+ 'FS_LC0_CM_RXDATA_0': '0x0',
+ 'FS_LC0_CM_RXDATA_1': '0x0',
+ 'FS_LC0_CM_TXDATA_0': '0x82000002',
+ 'FS_LC0_CM_TXDATA_1': '0x0',
+ 'FS_LC0_PKT_CNT_0': '0x0',
+ 'FS_LC0_PKT_CNT_1': '0x0',
+ 'FS_LC0_RDRPSCNT': '0x3e791',
+ 'FS_LC0_RERRSCNT': '0x0',
+ 'FS_LC0_RMCSCNT': '0x173b923',
+ 'FS_LC0_RPKTSCNT': '0x0',
+ 'FS_LC0_RUCSCNT': '0x43cab',
+ 'FS_LC0_SC_STAT': '0x0',
+ 'FS_LC0_STATE': '0x1033',
+ 'FS_LC0_TDRPSCNT': '0x0',
+ 'FS_LC0_TPKTSCNT': '0x1'}
+
:param link: The link to get stats for (0-4).
:type link: integer
@@ -1043,15 +1346,14 @@ class Node(object):
).replace('(link)', '').strip()
] = reg_value[1].strip()
- # Make sure we found something
- if (not results):
- raise TftpException("Node failed to reach TFTP server")
-
return results
def get_linkmap(self):
"""Gets the src and destination of each link on a node.
+ >>> node.get_linkmap()
+ {1: 2, 3: 1, 4: 3}
+
:return: Returns a map of link_id->node_id.
:rtype: dictionary
@@ -1059,12 +1361,9 @@ class Node(object):
:raises TftpException: If the TFTP transfer fails.
"""
- try:
- filename = self._run_fabric_command(
- function_name='fabric_info_get_link_map',
- )
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ filename = self._run_fabric_command(
+ function_name='fabric_info_get_link_map',
+ )
results = {}
for line in open(filename):
@@ -1074,15 +1373,14 @@ class Node(object):
node_id = int(elements[3].strip())
results[link_id] = node_id
- # Make sure we found something
- if (not results):
- raise TftpException("Node failed to reach TFTP server")
-
return results
def get_routing_table(self):
"""Gets the routing table as instantiated in the fabric switch.
+ >>> node.get_routing_table()
+ {1: [0, 0, 0, 3, 0], 2: [0, 3, 0, 0, 2], 3: [0, 2, 0, 0, 3]}
+
:return: Returns a map of node_id->rt_entries.
:rtype: dictionary
@@ -1090,12 +1388,9 @@ class Node(object):
:raises TftpException: If the TFTP transfer fails.
"""
- try:
- filename = self._run_fabric_command(
- function_name='fabric_info_get_routing_table',
- )
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ filename = self._run_fabric_command(
+ function_name='fabric_info_get_routing_table',
+ )
results = {}
for line in open(filename):
@@ -1107,16 +1402,17 @@ class Node(object):
rt_entries.append(int(entry))
results[node_id] = rt_entries
- # Make sure we found something
- if (not results):
- raise TftpException("Node failed to reach TFTP server")
-
return results
def get_depth_chart(self):
"""Gets a table indicating the distance from a given node to all other
nodes on each fabric link.
+ >>> node.get_depth_chart()
+ {1: {'shortest': (0, 0)},
+ 2: {'others': [(3, 1)], 'shortest': (0, 0)},
+ 3: {'others': [(2, 1)], 'shortest': (0, 0)}}
+
:return: Returns a map of target->(neighbor, hops),
[other (neighbors,hops)]
:rtype: dictionary
@@ -1125,12 +1421,9 @@ class Node(object):
:raises TftpException: If the TFTP transfer fails.
"""
- try:
- filename = self._run_fabric_command(
- function_name='fabric_info_get_depth_chart',
- )
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ filename = self._run_fabric_command(
+ function_name='fabric_info_get_depth_chart',
+ )
results = {}
for line in open(filename):
@@ -1142,21 +1435,20 @@ class Node(object):
dchrt_entries = {}
dchrt_entries['shortest'] = (neighbor, hops)
try:
- other_hops_neighbors = elements[12].strip().split('[,\s]+')
+ other_hops_neighbors = elements[12].strip().split(
+ r'[,\s]+'
+ )
hops = []
for entry in other_hops_neighbors:
pair = entry.strip().split('/')
hops.append((int(pair[1]), int(pair[0])))
dchrt_entries['others'] = hops
- except:
+
+ except Exception: # pylint: disable=W0703
pass
results[target] = dchrt_entries
- # Make sure we found something
- if (not results):
- raise TftpException("Node failed to reach TFTP server")
-
return results
def get_server_ip(self, interface=None, ipv6=False, user="user1",
@@ -1180,8 +1472,10 @@ class Node(object):
:return: The IP address of the server.
:rtype: string
- :raises IpmiError: If errors in the command occur with BMC communication.
- :raises IPDiscoveryError: If the server is off, or the IP can't be obtained.
+ :raises IpmiError: If errors in the command occur with BMC \
+communication.
+ :raises IPDiscoveryError: If the server is off, or the IP can't be \
+obtained.
"""
verbosity = 2 if self.verbose else 0
@@ -1197,7 +1491,7 @@ class Node(object):
or if sent to a primary node, the linkspeed setting for the
Profile 0 of the currently active Configuration.
- >>> fabric.get_linkspeed()
+ >>> node.get_linkspeed()
2.5
:param link: The fabric link number to read the linkspeed for.
@@ -1205,20 +1499,17 @@ class Node(object):
:param actual: WhetherThe fabric link number to read the linkspeed for.
:type actual: boolean
- :return: Linkspeed for the fabric..
+ :return: Linkspeed for the fabric.
:rtype: float
"""
- try:
- return self.bmc.fabric_get_linkspeed(link=link, actual=actual)
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ return self.bmc.fabric_get_linkspeed(link=link, actual=actual)
def get_uplink(self, iface=0):
"""Get the uplink a MAC will use when transmitting a packet out of the
cluster.
- >>> fabric.get_uplink(iface=1)
+ >>> node.get_uplink(iface=1)
0
:param iface: The interface for the uplink.
@@ -1230,10 +1521,7 @@ class Node(object):
:raises IpmiError: When any errors are encountered.
"""
- try:
- return self.bmc.fabric_config_get_uplink(iface=iface)
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ return self.bmc.fabric_config_get_uplink(iface=iface)
def set_uplink(self, uplink=0, iface=0):
"""Set the uplink a MAC will use when transmitting a packet out of the
@@ -1242,7 +1530,7 @@ class Node(object):
>>> #
>>> # Set eth0 to uplink 1 ...
>>> #
- >>> fabric.set_uplink(uplink=1,iface=0)
+ >>> node.set_uplink(uplink=1,iface=0)
:param uplink: The uplink to set.
:type uplink: integer
@@ -1252,13 +1540,54 @@ class Node(object):
:raises IpmiError: When any errors are encountered.
"""
- try:
- return self.bmc.fabric_config_set_uplink(
- uplink=uplink,
- iface=iface
- )
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ return self.bmc.fabric_config_set_uplink(
+ uplink=uplink,
+ iface=iface
+ )
+
+ def get_uplink_speed(self):
+ """Get the uplink speed of this node.
+
+ >>> node.get_uplink_speed()
+ 1
+
+ :return: The uplink speed of this node, in Gbps
+ :rtype: integer
+
+ """
+ return self.bmc.fabric_get_uplink_speed()
+
+ def get_uplink_info(self):
+ """Get the uplink information for this node.
+
+ >>> node.get_uplink_info()
+ 'Node 0: eth0 0, eth1 0, mgmt 0'
+
+ :return: The uplink information for this node
+ :rtype: string
+
+ """
+ return self.bmc.fabric_get_uplink_info().strip()
+
+ def read_fru(self, fru_number, offset=0, bytes_to_read=-1):
+ """Read from node's fru starting at offset.
+ This is equivalent to the ipmitool fru read command.
+
+ :param fru_number: FRU image to read
+ :type fru_number: integer
+ :param offset: File offset
+ :type offset: integer
+ :param bytes_to_read: Number of bytes to read
+ :type bytes_to_read: integer
+
+ :return: The data read from FRU
+ :rtype: string
+
+ """
+ with tempfile.NamedTemporaryFile(delete=True) as hexfile:
+ self.bmc.fru_read(fru_number, hexfile.name)
+ hexfile.seek(offset)
+ return(hexfile.read(bytes_to_read))
def _run_fabric_command(self, function_name, **kwargs):
"""Handles the basics of sending a node a command for fabric data."""
@@ -1268,16 +1597,12 @@ class Node(object):
getattr(self.bmc, function_name)(filename=basename, **kwargs)
self.ecme_tftp.get_file(basename, filename)
- except (IpmiError, TftpException) as e:
- try:
- getattr(self.bmc, function_name)(
- filename=basename,
- tftp_addr=self.tftp_address,
- **kwargs
- )
-
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ except (IpmiError, TftpException):
+ getattr(self.bmc, function_name)(
+ filename=basename,
+ tftp_addr=self.tftp_address,
+ **kwargs
+ )
deadline = time.time() + 10
while (time.time() < deadline):
@@ -1286,13 +1611,16 @@ class Node(object):
self.tftp.get_file(src=basename, dest=filename)
if (os.path.getsize(filename) > 0):
break
-
except (TftpException, IOError):
pass
+ if os.path.getsize(filename) == 0:
+ raise TftpException("Node failed to reach TFTP server")
+
return filename
- def _get_partition(self, fwinfo, image_type, partition_arg):
+ @staticmethod
+ def _get_partition(fwinfo, image_type, partition_arg):
"""Get a partition for this image type based on the argument."""
# Filter partitions for this type
partitions = [x for x in fwinfo if
@@ -1351,22 +1679,26 @@ class Node(object):
filename = image.render_to_simg(priority, daddr)
basename = os.path.basename(filename)
- try:
- self.bmc.register_firmware_write(basename, partition_id, image.type)
- self.ecme_tftp.put_file(filename, basename)
- except (IpmiError, TftpException):
+ for _ in xrange(2):
+ try:
+ self.bmc.register_firmware_write(
+ basename,
+ partition_id,
+ image.type
+ )
+ self.ecme_tftp.put_file(filename, basename)
+ break
+ except (IpmiError, TftpException):
+ pass
+ else:
# Fall back and use TFTP server
self.tftp.put_file(filename, basename)
result = self.bmc.update_firmware(basename, partition_id,
image.type, self.tftp_address)
- if (not hasattr(result, "tftp_handle_id")):
- raise AttributeError("Failed to start firmware upload")
self._wait_for_transfer(result.tftp_handle_id)
# Verify crc and activate
- result = self.bmc.check_firmware(partition_id)
- if ((not hasattr(result, "crc32")) or (result.error != None)):
- raise AttributeError("Node reported crc32 check failure")
+ self.bmc.check_firmware(partition_id)
self.bmc.activate_firmware(partition_id)
def _download_image(self, partition):
@@ -1376,15 +1708,21 @@ class Node(object):
partition_id = int(partition.partition)
image_type = partition.type.split()[1][1:-1]
- try:
- self.bmc.register_firmware_read(basename, partition_id, image_type)
- self.ecme_tftp.get_file(basename, filename)
- except (IpmiError, TftpException):
+ for _ in xrange(2):
+ try:
+ self.bmc.register_firmware_read(
+ basename,
+ partition_id,
+ image_type
+ )
+ self.ecme_tftp.get_file(basename, filename)
+ break
+ except (IpmiError, TftpException):
+ pass
+ else:
# Fall back and use TFTP server
result = self.bmc.retrieve_firmware(basename, partition_id,
image_type, self.tftp_address)
- if (not hasattr(result, "tftp_handle_id")):
- raise AttributeError("Failed to start firmware download")
self._wait_for_transfer(result.tftp_handle_id)
self.tftp.get_file(basename, filename)
@@ -1396,17 +1734,12 @@ class Node(object):
"""Wait for a firmware transfer to finish."""
deadline = time.time() + 180
result = self.bmc.get_firmware_status(handle)
- if (not hasattr(result, "status")):
- raise AttributeError('Failed to retrieve firmware transfer status')
while (result.status == "In progress"):
if (time.time() >= deadline):
raise TimeoutError("Transfer timed out after 3 minutes")
time.sleep(1)
result = self.bmc.get_firmware_status(handle)
- if (not hasattr(result, "status")):
- raise AttributeError(
- "Failed to retrieve firmware transfer status")
if (result.status != "Complete"):
raise TransferFailure("Node reported TFTP transfer failure")
@@ -1415,6 +1748,8 @@ class Node(object):
"""Check if this host is ready for an update."""
info = self.get_versions()
fwinfo = self.get_firmware_info()
+ num_ubootenv_partitions = len([x for x in fwinfo
+ if "UBOOTENV" in x.type])
# Check firmware version
if package.version and info.firmware_version:
@@ -1440,13 +1775,9 @@ class Node(object):
% (required_version, ecme_version))
# Check slot0 vs. slot2
- # TODO: remove this check
if (package.config and info.firmware_version != "Unknown" and
- len(info.firmware_version) < 32):
- if "slot2" in info.firmware_version:
- firmware_config = "slot2"
- else:
- firmware_config = "default"
+ len(info.firmware_version) < 32):
+ firmware_config = "default"
if (package.config != firmware_config):
raise FirmwareConfigError(
@@ -1459,7 +1790,8 @@ class Node(object):
# Check partitions
for image in package.images:
- if ((image.type == "UBOOTENV") or (partition_arg == "BOTH")):
+ if (image.type == "UBOOTENV" and num_ubootenv_partitions >= 2
+ or partition_arg == "BOTH"):
partitions = [self._get_partition(fwinfo, image.type, x)
for x in ["FIRST", "SECOND"]]
else:
@@ -1475,11 +1807,16 @@ class Node(object):
if (image.type in ["CDB", "BOOT_LOG"] and
partition.in_use == "1"):
raise PartitionInUseError(
- "Can't upload to a CDB/BOOT_LOG partition that's in use")
+ "Can't upload to a CDB/BOOT_LOG partition " +
+ "that's in use"
+ )
- return True
+ # Try a TFTP download. Would try an upload too, but nowhere to put it.
+ partition = self._get_partition(fwinfo, "SOC_ELF", "FIRST")
+ self._download_image(partition)
- def _get_next_priority(self, fwinfo, package):
+ @staticmethod
+ def _get_next_priority(fwinfo, package):
""" Get the next priority """
priority = None
image_types = [x.type for x in package.images]
@@ -1493,15 +1830,5 @@ class Node(object):
"Unable to increment SIMG priority, too high")
return priority
- def _parse_ipmierror(self, error_details):
- """Parse a meaningful message from an IpmiError """
- try:
- error = str(error_details).lstrip().splitlines()[0].rstrip()
- if (error.startswith('Error: ')):
- error = error[7:]
- return error
- except IndexError:
- return 'Unknown IPMItool error.'
-
# End of file: ./node.py