From a80f121ee4ca3f94d45714381fb3032bbf5835bf Mon Sep 17 00:00:00 2001 From: George Kraft Date: Wed, 4 Sep 2013 11:18:13 -0500 Subject: CXMAN-221: Fix imports for the cxmanage_api.cli commands --- cxmanage_api/cli/commands/power.py | 2 +- cxmanage_api/cli/commands/sensor.py | 2 +- cxmanage_api/cli/commands/tspackage.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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): -- cgit v1.2.1 From eee4cd77c9709b1de4fe447f398d10593e4ba30c Mon Sep 17 00:00:00 2001 From: Greg Lutostanski Date: Thu, 26 Sep 2013 12:31:22 -0500 Subject: CXMAN-230: adding a tmux cxmanage session muxer This will hopefully replace sol_tabs eventually --- scripts/cxmux | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100755 scripts/cxmux diff --git a/scripts/cxmux b/scripts/cxmux new file mode 100755 index 0000000..17150c2 --- /dev/null +++ b/scripts/cxmux @@ -0,0 +1,51 @@ +#!/usr/bin/env python + +import os +import sys +import cxmanage_api.fabric +from optparse import OptionParser + +def main(): + parser = OptionParser("usage: %prog [options] COMMAND ecmeIP", conflict_handler="resolve") + parser.add_option("-s", "--ssh", + action="store_const", const=True, dest="ssh", default=False, + help="Use the SPU IPs rather than ECME IPs") + parser.add_option("-n", "--nosync", + action="store_const", const=False, dest="sync", default=True, + help="Do not syncronize input across terminals") + parser.disable_interspersed_args() + (options, args) = parser.parse_args() + if len(args) == 0: + parser.print_help() + return -1 + elif len(args) < 2: + parser.error("Need to specify COMMAND and ecmeIP") + + command = " ".join(args[:-1]) + ecmeip = args[-1] + name = '%s@%s' % (args[0], ecmeip) + fabric = cxmanage_api.fabric.Fabric(ecmeip) + ips = [node.ip_address for node in fabric.nodes.values()] + if options.ssh: + ips = fabric.get_server_ip().values() + + for i, ip in enumerate(ips): + if i == 0: + os.system('tmux new-window -n "%s"' % name) + os.system('tmux send-keys -l "%s %s"' % (command, ip)) + os.system('tmux send-keys Enter') + continue + + os.system('tmux split-window -h') + os.system('tmux send-keys -l "%s %s"' % (command, ip)) + os.system('tmux send-keys Enter') + os.system('tmux select-layout -t "%s" even-horizontal >/dev/null' % name) + + os.system('tmux select-layout -t "%s" tiled >/dev/null' % name) + if options.sync: + os.system('tmux set-window-option -t "%s" synchronize-panes on >/dev/null' % name) + + return 0 + +if __name__ == '__main__': + sys.exit(main()) -- cgit v1.2.1 From c38aa01a17663f0eeecfa209554aa77d293b0d9b Mon Sep 17 00:00:00 2001 From: "matthew.hodgins" Date: Tue, 24 Sep 2013 15:50:37 -0500 Subject: CXMAN-228 simplify updating EEPROM --- cxmanage_api/cli/__init__.py | 2 +- cxmanage_api/cli/commands/eeprom.py | 130 ++++++++++++++++++++++++++++++++++++ cxmanage_api/cx_exceptions.py | 24 +++++++ cxmanage_api/node.py | 79 +++++++++++++++++++++- scripts/cxmanage | 48 +++++++++---- 5 files changed, 266 insertions(+), 17 deletions(-) create mode 100644 cxmanage_api/cli/commands/eeprom.py 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/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 "", line 1, in + 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/node.py b/cxmanage_api/node.py index 358af95..b0d7922 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 @@ -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/scripts/cxmanage b/scripts/cxmanage index ce7748c..93518dd 100755 --- a/scripts/cxmanage +++ b/scripts/cxmanage @@ -50,6 +50,7 @@ from cxmanage_api.cli.commands.ipdiscover import ipdiscover_command from cxmanage_api.cli.commands.tspackage import tspackage_command from cxmanage_api.cli.commands.fru_version import node_fru_version_command, \ slot_fru_version_command +from cxmanage_api.cli.commands.eeprom import eepromupdate_command PYIPMI_VERSION = '0.8.0' @@ -86,6 +87,10 @@ FWUPDATE_IMAGE_TYPES = ['PACKAGE'] + sorted([ 'DIAG_ELF', ]) +EEPROMUPDATE_EPILOG = """examples: + cxmanage -a eepromupdate slot tn_storage.single_slot_v3.0.0.img 192.168.1.1 + cxmanage -a eepromupdate node dual_uplink_node_0.img \ +dual_uplink_node_1.img dual_node_0.img dual_node_0.img 192.168.1.1""" def build_parser(): @@ -95,7 +100,7 @@ def build_parser(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog=PARSER_EPILOG) - #global arguments + # global arguments parser.add_argument('-V', '--version', action='store_true', help='Show version information') parser.add_argument('-u', '--user', default='admin', @@ -134,7 +139,7 @@ def build_parser(): subparsers = parser.add_subparsers() - #power command + # power command power = subparsers.add_parser('power', help='control server power') power_subs = power.add_subparsers() @@ -172,26 +177,26 @@ def build_parser(): 'status', help='get the current power policy') power_policy_status.set_defaults(func=power_policy_status_command) - #mcreset command + # mcreset command mcreset = subparsers.add_parser('mcreset', help='reset the management controller') mcreset.set_defaults(func=mcreset_command) - #fwupdate command + # fwupdate command fwupdate = subparsers.add_parser('fwupdate', help='update firmware', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=FWUPDATE_EPILOG) fwupdate.add_argument('image_type', metavar='IMAGE_TYPE', help='image type to use (%s)' % ", ".join(FWUPDATE_IMAGE_TYPES), type=lambda string: string.upper(), - choices = FWUPDATE_IMAGE_TYPES) + choices=FWUPDATE_IMAGE_TYPES) fwupdate.add_argument('filename', help='path to file to upload') fwupdate.add_argument('--full', action='store_true', default=False, help='Update primary AND backup partitions (will reset MC)') fwupdate.add_argument('--partition', help='Specify partition to update', default='INACTIVE', type=lambda string: string.upper(), - choices = list([ + choices=list([ 'FIRST', 'SECOND', 'BOTH', @@ -219,27 +224,42 @@ def build_parser(): help='Version for SIMG header', default=None) fwupdate.set_defaults(func=fwupdate_command) - #fwinfo command + # eepromupdate command + eepromupdate = subparsers.add_parser('eepromupdate', help='update EEPROM', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=EEPROMUPDATE_EPILOG + ) + eepromupdate.add_argument('eeprom_location', + choices=['slot', 'node'], + help='EEPROM location' + ) + eepromupdate.add_argument('images', + nargs='+', + help='path to file(s) to upload' + ) + eepromupdate.set_defaults(func=eepromupdate_command) + + # fwinfo command fwinfo = subparsers.add_parser('fwinfo', help='get FW info') fwinfo.set_defaults(func=fwinfo_command) - #sensor command + # sensor command sensor = subparsers.add_parser('sensor', help='read sensor value') sensor.add_argument('sensor_name', help='Sensor name to read', nargs='?', default='') sensor.set_defaults(func=sensor_command) - #ipinfo command + # ipinfo command ipinfo = subparsers.add_parser('ipinfo', help='get IP info') ipinfo.set_defaults(func=ipinfo_command) - #macaddrs command + # macaddrs command macaddrs = subparsers.add_parser('macaddrs', help='get mac addresses') macaddrs.set_defaults(func=macaddrs_command) - #config command + # config command config = subparsers.add_parser('config', help='configure hosts') config_subs = config.add_subparsers() @@ -258,14 +278,14 @@ def build_parser(): pxe.add_argument('interface', help='pxe interface to use') pxe.set_defaults(func=config_pxe_command) - #info command + # info command info = subparsers.add_parser('info', help='get host info') info.add_argument('info_type', nargs='?', type=lambda string: string.lower(), choices=['basic', 'ubootenv']) info.set_defaults(func=info_command) - #ipmitool command + # ipmitool command ipmitool = subparsers.add_parser('ipmitool', help='run an arbitrary ipmitool command') ipmitool.add_argument('-l', '--lanplus', @@ -275,7 +295,7 @@ def build_parser(): help='ipmitool arguments') ipmitool.set_defaults(func=ipmitool_command) - #ipdiscover command + # ipdiscover command ipdiscover = subparsers.add_parser('ipdiscover', help='discover server-side IP addresses') ipdiscover.add_argument('-A', '--aggressive', action='store_true', -- cgit v1.2.1 From 25d3656b4837842e6b06a65e88a00a977eb3e198 Mon Sep 17 00:00:00 2001 From: George Kraft Date: Tue, 1 Oct 2013 15:13:33 -0500 Subject: CXMAN-225: Retry obtaining TftpServer port if we get a socket.error Seems like a race that occurs between the main thread and TftpServer thread. --- cxmanage_api/tftp.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/cxmanage_api/tftp.py b/cxmanage_api/tftp.py index 0b33db8..91671cc 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 @@ -74,10 +75,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=1) + 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. """ -- cgit v1.2.1 From 15433b9d93150d212562be13a2b2f10e8b9dd9b3 Mon Sep 17 00:00:00 2001 From: George Kraft Date: Tue, 1 Oct 2013 15:35:04 -0500 Subject: CXMAN-238: Only spawn a single InternalTftp instance by default I don't think each node needs its own TFTP server. --- cxmanage_api/fabric.py | 2 +- cxmanage_api/node.py | 2 +- cxmanage_api/tftp.py | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) 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 b0d7922..f720d28 100644 --- a/cxmanage_api/node.py +++ b/cxmanage_api/node.py @@ -88,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): diff --git a/cxmanage_api/tftp.py b/cxmanage_api/tftp.py index 91671cc..58b6cee 100644 --- a/cxmanage_api/tftp.py +++ b/cxmanage_api/tftp.py @@ -62,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__() -- cgit v1.2.1 From 52057843f2b4002095422069ae0abbfa360de9ed Mon Sep 17 00:00:00 2001 From: George Kraft Date: Tue, 1 Oct 2013 15:41:26 -0500 Subject: CXMAN-225: Increase InternalTftp port deadline to 10 seconds --- cxmanage_api/tftp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cxmanage_api/tftp.py b/cxmanage_api/tftp.py index 58b6cee..59e9774 100644 --- a/cxmanage_api/tftp.py +++ b/cxmanage_api/tftp.py @@ -85,7 +85,7 @@ class InternalTftp(Thread): # Get the port we actually hosted on if port == 0: - deadline = datetime.now() + timedelta(seconds=1) + deadline = datetime.now() + timedelta(seconds=10) while datetime.now() < deadline: try: self.port = self.server.sock.getsockname()[1] -- cgit v1.2.1