diff options
Diffstat (limited to 'cxmanage_api')
-rw-r--r-- | cxmanage_api/cli/__init__.py | 2 | ||||
-rw-r--r-- | cxmanage_api/cli/commands/eeprom.py | 130 | ||||
-rw-r--r-- | cxmanage_api/cli/commands/power.py | 2 | ||||
-rw-r--r-- | cxmanage_api/cli/commands/sensor.py | 2 | ||||
-rw-r--r-- | cxmanage_api/cli/commands/tspackage.py | 2 | ||||
-rw-r--r-- | cxmanage_api/cx_exceptions.py | 24 | ||||
-rw-r--r-- | cxmanage_api/fabric.py | 2 | ||||
-rw-r--r-- | cxmanage_api/node.py | 81 | ||||
-rw-r--r-- | cxmanage_api/tftp.py | 25 |
9 files changed, 258 insertions, 12 deletions
diff --git a/cxmanage_api/cli/__init__.py b/cxmanage_api/cli/__init__.py index 438d568..2d98d8a 100644 --- a/cxmanage_api/cli/__init__.py +++ b/cxmanage_api/cli/__init__.py @@ -124,7 +124,7 @@ def get_nodes(args, tftp, verify_prompt=False): print( "NOTE: Please check node count! Ensure discovery of all " + "nodes in the cluster. Power cycle your system if the " + - "discovered node count does not equal nodes in" + + "discovered node count does not equal nodes in " + "your system.\n" ) if not prompt_yes("Discovered %i nodes. Continue?" diff --git a/cxmanage_api/cli/commands/eeprom.py b/cxmanage_api/cli/commands/eeprom.py new file mode 100644 index 0000000..fa715ff --- /dev/null +++ b/cxmanage_api/cli/commands/eeprom.py @@ -0,0 +1,130 @@ +"""Calxeda: eeprom.py """ + +# Copyright (c) 2012, Calxeda Inc. +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Calxeda Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. + +from cxmanage_api.cli import get_nodes, get_tftp, run_command, prompt_yes + +def eepromupdate_command(args): + """Updates the EEPROM's on a cluster or host""" + def validate_config(): + """Makes sure the system type is applicable to EEPROM updates""" + for node in nodes: + if('Dual Node' not in node.get_versions().hardware_version): + print 'ERROR: eepromupdate is only valid on TerraNova systems' + return True + + return False + + def validate_images(): + """Makes sure all the necessary images have been provided""" + if(args.eeprom_location == 'node'): + for node in nodes: + node_hw_ver = node.get_versions().hardware_version + if('Uplink' in node_hw_ver): + image = 'dual_uplink_node_%s' % (node.node_id % 4) + else: + image = 'dual_node_%s' % (node.node_id % 4) + if(not [img for img in args.images if image in img]): + print 'ERROR: no valid image for node %s' % node.node_id + return True + + else: + image = args.images[0] + if('tn_storage.single_slot' not in image): + print 'ERROR: %s is an invalid image for slot EEPROM' % image + return True + + return False + + def do_update(): + """Updates the EEPROM images""" + if(args.eeprom_location == 'node'): + for node in nodes: + node_hw_ver = node.get_versions().hardware_version + if('Uplink' in node_hw_ver): + needed_image = 'dual_uplink_node_%s' % (node.node_id % 4) + else: + needed_image = 'dual_node_%s' % (node.node_id % 4) + image = [img for img in args.images if needed_image in img][0] + print 'Updating node EEPROM on node %s' % node.node_id + if(args.verbose): + print ' %s' % image + try: + node.update_node_eeprom(image) + except Exception as err: + print 'ERROR: %s' % str(err) + return True + + print '' # for readability + else: + image = args.images[0] + # First node in every slot gets the slot image + slot_nodes = [node for node in nodes if node.node_id % 4 == 0] + _, errors = run_command( + args, slot_nodes, "update_slot_eeprom", image + ) + if(errors): + print 'ERROR: EEPROM update failed' + return True + + return False + + if not args.all_nodes: + if args.force: + print( + 'WARNING: Updating EEPROM without --all-nodes' + + ' is dangerous.' + ) + else: + if not prompt_yes( + 'WARNING: Updating EEPROM without ' + + '--all-nodes is dangerous. Continue?' + ): + return 1 + + tftp = get_tftp(args) + nodes = get_nodes(args, tftp, verify_prompt=True) + + errors = validate_config() + + if(not errors): + errors = validate_images() + + if(not errors): + errors = do_update() + + if not args.quiet and not errors: + print "Command completed successfully." + print "A power cycle is required for the update to take effect.\n" + + return errors + + diff --git a/cxmanage_api/cli/commands/power.py b/cxmanage_api/cli/commands/power.py index 623c38d..1255cbc 100644 --- a/cxmanage_api/cli/commands/power.py +++ b/cxmanage_api/cli/commands/power.py @@ -31,7 +31,7 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. -from cxmanage import get_tftp, get_nodes, get_node_strings, run_command +from cxmanage_api.cli import get_tftp, get_nodes, get_node_strings, run_command def power_command(args): diff --git a/cxmanage_api/cli/commands/sensor.py b/cxmanage_api/cli/commands/sensor.py index 3a27143..acbad6e 100644 --- a/cxmanage_api/cli/commands/sensor.py +++ b/cxmanage_api/cli/commands/sensor.py @@ -32,7 +32,7 @@ # DAMAGE. -from cxmanage import get_tftp, get_nodes, get_node_strings, run_command +from cxmanage_api.cli import get_tftp, get_nodes, get_node_strings, run_command # pylint: disable=R0914 def sensor_command(args): diff --git a/cxmanage_api/cli/commands/tspackage.py b/cxmanage_api/cli/commands/tspackage.py index d6ee198..e4ccb51 100644 --- a/cxmanage_api/cli/commands/tspackage.py +++ b/cxmanage_api/cli/commands/tspackage.py @@ -46,7 +46,7 @@ import shutil import tarfile import tempfile -from cxmanage import get_tftp, get_nodes, run_command, COMPONENTS +from cxmanage_api.cli import get_tftp, get_nodes, run_command, COMPONENTS def tspackage_command(args): diff --git a/cxmanage_api/cx_exceptions.py b/cxmanage_api/cx_exceptions.py index df2dcc7..5f60df7 100644 --- a/cxmanage_api/cx_exceptions.py +++ b/cxmanage_api/cx_exceptions.py @@ -46,6 +46,30 @@ from tftpy.TftpShared import TftpException # Defines the custom exceptions used by the cxmanage_api project. # +class EEPROMUpdateError(Exception): + """Raised when an error is encountered while updating the EEPROM + + >>> from cxmanage_api.cx_exceptions import TimeoutError + >>> raise TimeoutError('My custom exception text!') + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + cxmanage_api.cx_exceptions.TimeoutError: My custom exception text! + + :param msg: Exceptions message and details to return to the user. + :type msg: string + :raised: When an error is encountered while updating the EEPROM + + """ + + def __init__(self, msg): + """Default constructor for the EEPROMUpdateError class.""" + super(EEPROMUpdateError, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + return self.msg + class TimeoutError(Exception): """Raised when a timeout has been reached. diff --git a/cxmanage_api/fabric.py b/cxmanage_api/fabric.py index 8a42ea7..564c97f 100644 --- a/cxmanage_api/fabric.py +++ b/cxmanage_api/fabric.py @@ -156,7 +156,7 @@ class Fabric(object): """ if (not self._tftp): - self._tftp = InternalTftp() + self._tftp = InternalTftp.default() return self._tftp diff --git a/cxmanage_api/node.py b/cxmanage_api/node.py index f0239ba..e4d3e31 100644 --- a/cxmanage_api/node.py +++ b/cxmanage_api/node.py @@ -52,7 +52,8 @@ from cxmanage_api.ip_retriever import IPRetriever as IPRETRIEVER from cxmanage_api.cx_exceptions import TimeoutError, NoSensorError, \ SocmanVersionError, FirmwareConfigError, PriorityIncrementError, \ NoPartitionError, TransferFailure, ImageSizeError, \ - PartitionInUseError, UbootenvError, NoFRUVersionError + PartitionInUseError, UbootenvError, NoFRUVersionError, \ + EEPROMUpdateError # pylint: disable=R0902, R0904 @@ -87,7 +88,7 @@ class Node(object): 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): @@ -839,6 +840,80 @@ communication. 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. @@ -1783,7 +1858,7 @@ obtained. "Unable to increment SIMG priority, too high") return priority - def _read_fru(self, fru_number, offset=0, bytes_to_read= -1): + 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. diff --git a/cxmanage_api/tftp.py b/cxmanage_api/tftp.py index 0b33db8..59e9774 100644 --- a/cxmanage_api/tftp.py +++ b/cxmanage_api/tftp.py @@ -37,6 +37,7 @@ import socket import logging import traceback +from datetime import datetime, timedelta from tftpy import TftpClient, TftpServer, setLogLevel from threading import Thread from cxmanage_api import temp_dir @@ -61,6 +62,14 @@ class InternalTftp(Thread): :type verbose: boolean """ + _default = None + + @staticmethod + def default(): + """ Return the default InternalTftp server """ + if InternalTftp._default == None: + InternalTftp._default = InternalTftp() + return InternalTftp._default def __init__(self, ip_address=None, port=0, verbose=False): super(InternalTftp, self).__init__() @@ -74,10 +83,18 @@ class InternalTftp(Thread): self.port = port self.start() - # Get the port we actually hosted on (this covers the port=0 case) - while not self.server.sock: - pass - self.port = self.server.sock.getsockname()[1] + # Get the port we actually hosted on + if port == 0: + deadline = datetime.now() + timedelta(seconds=10) + while datetime.now() < deadline: + try: + self.port = self.server.sock.getsockname()[1] + break + except (AttributeError, socket.error): + pass + else: + # don't catch the error on our last attempt + self.port = self.server.sock.getsockname()[1] def run(self): """ Run the server. Listens indefinitely. """ |