diff options
Diffstat (limited to 'cxmanage_api/node.py')
-rw-r--r-- | cxmanage_api/node.py | 881 |
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 |