diff options
author | Daniel Silverstone <daniel.silverstone@codethink.co.uk> | 2013-11-19 17:06:33 +0000 |
---|---|---|
committer | Daniel Silverstone <daniel.silverstone@codethink.co.uk> | 2013-11-19 17:06:33 +0000 |
commit | 54116d3c486ed7aa49f284e4cad9e6e7c293bea6 (patch) | |
tree | fa32a75ebba262def85af97cc34a7db69bc69034 | |
parent | 7087e92d40310d9bb6c8b4a6bb1baf7c3b73bee7 (diff) | |
parent | 4da2f9c7eaa95ebf357eeca3a497b6a206675ef8 (diff) | |
download | cxmanage-baserock/morph.tar.gz |
Merge tag 'v0.10.2' into baserock/morphbaserock/morph
v0.10 post-release tag for pyinstaller support
43 files changed, 3324 insertions, 712 deletions
@@ -1,3 +1,5 @@ tags *.pyc cxmanage.egg-info +.project +.pydevproject @@ -1,4 +1,4 @@ -Copyright (c) 2012, Calxeda Inc. +Copyright (c) 2012-2013, Calxeda Inc. All rights reserved. diff --git a/cxmanage_api/__init__.py b/cxmanage_api/__init__.py index 2228b38..c676a78 100644 --- a/cxmanage_api/__init__.py +++ b/cxmanage_api/__init__.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: __init__.py """ + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -35,6 +38,9 @@ import shutil import tempfile +__version__ = "0.10.2" + + WORK_DIR = tempfile.mkdtemp(prefix="cxmanage_api-") atexit.register(lambda: shutil.rmtree(WORK_DIR)) @@ -47,8 +53,8 @@ def temp_file(): :rtype: string """ - fd, filename = tempfile.mkstemp(dir=WORK_DIR) - os.close(fd) + file_, filename = tempfile.mkstemp(dir=WORK_DIR) + os.close(file_) return filename def temp_dir(): diff --git a/cxmanage/__init__.py b/cxmanage_api/cli/__init__.py index 50b760a..f57a394 100644 --- a/cxmanage/__init__.py +++ b/cxmanage_api/cli/__init__.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: __init__.py """ + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,6 +31,7 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. + import sys import time @@ -37,6 +41,24 @@ from cxmanage_api.tasks import TaskQueue from cxmanage_api.cx_exceptions import TftpException +COMPONENTS = [ + ("ecme_version", "ECME version"), + ("cdb_version", "CDB version"), + ("stage2_version", "Stage2boot version"), + ("bootlog_version", "Bootlog version"), + ("a9boot_version", "A9boot version"), + ("a15boot_version", "A15boot version"), + ("uboot_version", "Uboot version"), + ("ubootenv_version", "Ubootenv version"), + ("dtb_version", "DTB version"), + ("node_eeprom_version", "Node EEPROM version"), + ("node_eeprom_config", "Node EEPROM config"), + ("slot_eeprom_version", "Slot EEPROM version"), + ("slot_eeprom_config", "Slot EEPROM config"), + ("pmic_version", "PMIC version") +] + + def get_tftp(args): """Get a TFTP server""" if args.internal_tftp: @@ -71,7 +93,7 @@ def get_tftp(args): return InternalTftp(verbose=args.verbose) - +# pylint: disable=R0912 def get_nodes(args, tftp, verify_prompt=False): """Get nodes""" hosts = [] @@ -84,7 +106,7 @@ def get_nodes(args, tftp, verify_prompt=False): if args.all_nodes: if not args.quiet: - print "Getting IP addresses..." + print("Getting IP addresses...") results, errors = run_command(args, nodes, "get_fabric_ipinfo") @@ -92,8 +114,6 @@ def get_nodes(args, tftp, verify_prompt=False): for node in nodes: if node in results: for node_id, ip_address in sorted(results[node].iteritems()): - # TODO: make this more efficient. We can use a set of IP - # addresses instead of searching a list every time... new_node = Node(ip_address=ip_address, username=args.user, password=args.password, tftp=tftp, ecme_tftp_port=args.ecme_tftp_port, @@ -104,13 +124,13 @@ def get_nodes(args, tftp, verify_prompt=False): node_strings = get_node_strings(args, all_nodes, justify=False) if not args.quiet and all_nodes: - print "Discovered the following IP addresses:" + print("Discovered the following IP addresses:") for node in all_nodes: print node_strings[node] print if errors: - print "ERROR: Failed to get IP addresses. Aborting.\n" + print("ERROR: Failed to get IP addresses. Aborting.\n") sys.exit(1) if args.nodes: @@ -119,9 +139,12 @@ def get_nodes(args, tftp, verify_prompt=False): % (len(all_nodes), args.nodes)) sys.exit(1) elif verify_prompt and not args.force: - print "NOTE: Please check node count! Ensure discovery of all nodes in the cluster." - print "Power cycle your system if the discovered node count does not equal nodes in" - print "your system.\n" + 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 " + + "your system.\n" + ) if not prompt_yes("Discovered %i nodes. Continue?" % len(all_nodes)): sys.exit(1) @@ -135,6 +158,7 @@ def get_node_strings(args, nodes, justify=False): """ Get string representations for the nodes. """ # Use the private _node_id instead of node_id. Strange choice, # but we want to avoid accidentally polling the BMC. + # pylint: disable=W0212 if args.ids and all(x._node_id != None for x in nodes): strings = ["Node %i (%s)" % (x._node_id, x.ip_address) for x in nodes] else: @@ -147,7 +171,9 @@ def get_node_strings(args, nodes, justify=False): return dict(zip(nodes, strings)) +# pylint: disable=R0915 def run_command(args, nodes, name, *method_args): + """Runs a command on nodes.""" if args.threads != None: task_queue = TaskQueue(threads=args.threads, delay=args.command_delay) else: @@ -182,11 +208,13 @@ def run_command(args, nodes, name, *method_args): elif task.status == "Failed": errors[node] = task.error else: - errors[node] = KeyboardInterrupt("Aborted by keyboard interrupt") + errors[node] = KeyboardInterrupt( + "Aborted by keyboard interrupt" + ) if not args.quiet: _print_command_status(tasks, counter) - print "\n" + print("\n") # Handle errors should_retry = False @@ -206,9 +234,9 @@ def run_command(args, nodes, name, *method_args): elif args.retry >= 1: should_retry = True if args.retry == 1: - print "Retrying command 1 more time..." + print("Retrying command 1 more time...") elif args.retry > 1: - print "Retrying command %i more times..." % args.retry + print("Retrying command %i more times..." % args.retry) args.retry -= 1 if should_retry: @@ -220,6 +248,7 @@ def run_command(args, nodes, name, *method_args): def prompt_yes(prompt): + """Prompts the user. """ sys.stdout.write("%s (y/n) " % prompt) sys.stdout.flush() while True: @@ -232,8 +261,11 @@ def prompt_yes(prompt): return False -def parse_host_entry(entry, hostfiles=set()): +def parse_host_entry(entry, hostfiles=None): """parse a host entry""" + if not(hostfiles): + hostfiles = set() + try: return parse_hostfile_entry(entry, hostfiles) except ValueError: @@ -243,8 +275,11 @@ def parse_host_entry(entry, hostfiles=set()): return [entry] -def parse_hostfile_entry(entry, hostfiles=set()): +def parse_hostfile_entry(entry, hostfiles=None): """parse a hostfile entry, returning a list of hosts""" + if not(hostfiles): + hostfiles = set() + if entry.startswith('file='): filename = entry[5:] elif entry.startswith('hostfile='): @@ -275,12 +310,13 @@ def parse_ip_range_entry(entry): start, end = entry.split('-') # Convert start address to int - start_bytes = map(int, start.split('.')) + start_bytes = [int(x) for x in start.split('.')] + start_i = ((start_bytes[0] << 24) | (start_bytes[1] << 16) | (start_bytes[2] << 8) | (start_bytes[3])) # Convert end address to int - end_bytes = map(int, end.split('.')) + end_bytes = [int(x) for x in end.split('.')] end_i = ((end_bytes[0] << 24) | (end_bytes[1] << 16) | (end_bytes[2] << 8) | (end_bytes[3])) @@ -300,17 +336,20 @@ def _print_errors(args, nodes, errors): """ Print errors if they occured """ if errors: node_strings = get_node_strings(args, nodes, justify=True) - print "Command failed on these hosts" + print("Command failed on these hosts") for node in nodes: if node in errors: - print "%s: %s" % (node_strings[node], errors[node]) + print("%s: %s" % (node_strings[node], errors[node])) print # Print a special message for TFTP errors if all(isinstance(x, TftpException) for x in errors.itervalues()): - print "There may be networking issues (when behind NAT) between the host (where" - print "cxmanage is running) and the Calxeda node when establishing a TFTP session." - print "Please refer to the documentation for more information.\n" + print( + "There may be networking issues (when behind NAT) between " + + "the host (where cxmanage is running) and the Calxeda node " + + "when establishing a TFTP session. Please refer to the " + + "documentation for more information.\n" + ) def _print_command_status(tasks, counter): diff --git a/cxmanage/commands/__init__.py b/cxmanage_api/cli/commands/__init__.py index 2160043..b1cbfbb 100644 --- a/cxmanage/commands/__init__.py +++ b/cxmanage_api/cli/commands/__init__.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: __init__.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # diff --git a/cxmanage/commands/config.py b/cxmanage_api/cli/commands/config.py index ca80928..23aa028 100644 --- a/cxmanage/commands/config.py +++ b/cxmanage_api/cli/commands/config.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: config.py """ + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,9 +31,11 @@ # 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.ubootenv import UbootEnv, validate_boot_args +from cxmanage_api.cli import get_tftp, get_nodes, get_node_strings, run_command + +from cxmanage_api.ubootenv import validate_boot_args, \ + validate_pxe_interface def config_reset_command(args): @@ -41,7 +46,7 @@ def config_reset_command(args): if not args.quiet: print "Sending config reset command..." - results, errors = run_command(args, nodes, "config_reset") + _, errors = run_command(args, nodes, "config_reset") if not args.quiet and not errors: print "Command completed successfully.\n" @@ -62,7 +67,7 @@ def config_boot_command(args): if not args.quiet: print "Setting boot order..." - results, errors = run_command(args, nodes, "set_boot_order", + _, errors = run_command(args, nodes, "set_boot_order", args.boot_order) if not args.quiet and not errors: @@ -72,6 +77,7 @@ def config_boot_command(args): def config_boot_status_command(args): + """Get boot status command.""" tftp = get_tftp(args) nodes = get_nodes(args, tftp) @@ -92,3 +98,49 @@ def config_boot_status_command(args): print "Some errors occured during the command.\n" return len(errors) > 0 + + +def config_pxe_command(args): + """set the PXE boot interface""" + if args.interface == "status": + return config_pxe_status_command(args) + + validate_pxe_interface(args.interface) + + tftp = get_tftp(args) + nodes = get_nodes(args, tftp) + + if not args.quiet: + print "Setting pxe interface..." + + _, errors = run_command(args, nodes, "set_pxe_interface", + args.interface) + + if not args.quiet and not errors: + print "Command completed successfully.\n" + + return len(errors) > 0 + + +def config_pxe_status_command(args): + """Gets pxe status.""" + tftp = get_tftp(args) + nodes = get_nodes(args, tftp) + + if not args.quiet: + print "Getting pxe interface..." + results, errors = run_command(args, nodes, "get_pxe_interface") + + # Print results + if results: + node_strings = get_node_strings(args, results, justify=True) + print "PXE interface" + for node in nodes: + if node in results: + print "%s: %s" % (node_strings[node], results[node]) + print + + if not args.quiet and errors: + print "Some errors occured during the command.\n" + + return len(errors) > 0 diff --git a/cxmanage_api/cli/commands/eeprom.py b/cxmanage_api/cli/commands/eeprom.py new file mode 100644 index 0000000..86ca1cd --- /dev/null +++ b/cxmanage_api/cli/commands/eeprom.py @@ -0,0 +1,130 @@ +"""Calxeda: eeprom.py """ + +# Copyright (c) 2012-2013, 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/commands/fabric.py b/cxmanage_api/cli/commands/fabric.py index 3bf84c2..0ee7805 100644 --- a/cxmanage/commands/fabric.py +++ b/cxmanage_api/cli/commands/fabric.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: fabric.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,7 +31,7 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. -from cxmanage import get_tftp, get_nodes, run_command +from cxmanage_api.cli import get_tftp, get_nodes, run_command def ipinfo_command(args): @@ -41,7 +44,7 @@ def ipinfo_command(args): if not args.quiet: print "Getting IP addresses..." - results, errors = run_command(args, nodes, "get_fabric_ipinfo") + results, _ = run_command(args, nodes, "get_fabric_ipinfo") for node in nodes: if node in results: diff --git a/cxmanage/commands/fw.py b/cxmanage_api/cli/commands/fw.py index 87f810b..6663152 100644 --- a/cxmanage/commands/fw.py +++ b/cxmanage_api/cli/commands/fw.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: fw.py """ + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,15 +30,16 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. + from pkg_resources import parse_version -from cxmanage import get_tftp, get_nodes, get_node_strings, run_command, \ - prompt_yes +from cxmanage_api.cli import get_tftp, get_nodes, get_node_strings, \ + run_command, prompt_yes from cxmanage_api.image import Image from cxmanage_api.firmware_package import FirmwarePackage - +# pylint: disable=R0912 def fwupdate_command(args): """update firmware on a cluster or host""" def do_update(): @@ -45,7 +48,7 @@ def fwupdate_command(args): if not args.quiet: print "Checking hosts..." - results, errors = run_command(args, nodes, "_check_firmware", + _, errors = run_command(args, nodes, "_check_firmware", package, args.partition, args.priority) if errors: print "ERROR: Firmware update aborted." @@ -54,8 +57,8 @@ def fwupdate_command(args): if not args.quiet: print "Updating firmware..." - results, errors = run_command(args, nodes, "update_firmware", package, - args.partition, args.priority) + _, errors = run_command(args, nodes, "update_firmware", package, + args.partition, args.priority) if errors: print "ERROR: Firmware update failed." return True @@ -78,7 +81,7 @@ def fwupdate_command(args): print "ERROR: MC reset is unsafe on ECME version v%s" % version print "Please power cycle the system and start a new fwupdate." return True - + if not args.quiet: print "Resetting nodes..." @@ -103,16 +106,21 @@ def fwupdate_command(args): args.skip_crc32, args.fw_version) package = FirmwarePackage() package.images.append(image) - except ValueError as e: - print "ERROR: %s" % e + except ValueError as err: + print "ERROR: %s" % err return True if not args.all_nodes: if args.force: - print 'WARNING: Updating firmware without --all-nodes is dangerous.' + print( + 'WARNING: Updating firmware without --all-nodes' + + ' is dangerous.' + ) else: if not prompt_yes( - 'WARNING: Updating firmware without --all-nodes is dangerous. Continue?'): + 'WARNING: Updating firmware without ' + + '--all-nodes is dangerous. Continue?' + ): return 1 tftp = get_tftp(args) diff --git a/cxmanage/commands/info.py b/cxmanage_api/cli/commands/info.py index d002906..0968351 100644 --- a/cxmanage/commands/info.py +++ b/cxmanage_api/cli/commands/info.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: info.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,7 +31,9 @@ # 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, COMPONENTS def info_command(args): @@ -41,16 +46,6 @@ def info_command(args): def info_basic_command(args): """Print basic info""" - components = [ - ("ecme_version", "ECME version"), - ("cdb_version", "CDB version"), - ("stage2_version", "Stage2boot version"), - ("bootlog_version", "Bootlog version"), - ("a9boot_version", "A9boot version"), - ("uboot_version", "Uboot version"), - ("ubootenv_version", "Ubootenv version"), - ("dtb_version", "DTB version") - ] tftp = get_tftp(args) nodes = get_nodes(args, tftp) @@ -64,13 +59,17 @@ def info_basic_command(args): for node in nodes: if node in results: result = results[node] + # Get mappings between attributes and formatted strings + components = COMPONENTS + print "[ Info from %s ]" % node_strings[node] - print "Hardware version : %s" % result.hardware_version - print "Firmware version : %s" % result.firmware_version + print "Hardware version : %s" % result.hardware_version + print "Firmware version : %s" % result.firmware_version + # var is the variable, string is the printable string of var for var, string in components: if hasattr(result, var): version = getattr(result, var) - print "%s: %s" % (string.ljust(19), version) + print "%s: %s" % (string.ljust(20), version) print if not args.quiet and errors: @@ -80,6 +79,7 @@ def info_basic_command(args): def info_ubootenv_command(args): + """Print uboot info""" tftp = get_tftp(args) nodes = get_nodes(args, tftp) diff --git a/cxmanage/commands/ipdiscover.py b/cxmanage_api/cli/commands/ipdiscover.py index f619d16..fd21546 100644 --- a/cxmanage/commands/ipdiscover.py +++ b/cxmanage_api/cli/commands/ipdiscover.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: ipdiscover.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,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 ipdiscover_command(args): diff --git a/cxmanage/commands/ipmitool.py b/cxmanage_api/cli/commands/ipmitool.py index f8baf80..ab95bbf 100644 --- a/cxmanage/commands/ipmitool.py +++ b/cxmanage_api/cli/commands/ipmitool.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: ipmitool.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,8 +31,8 @@ # 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 ipmitool_command(args): """run arbitrary ipmitool command""" diff --git a/cxmanage/commands/mc.py b/cxmanage_api/cli/commands/mc.py index 2573540..86e0963 100644 --- a/cxmanage/commands/mc.py +++ b/cxmanage_api/cli/commands/mc.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: mc.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,7 +31,8 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. -from cxmanage import get_tftp, get_nodes, run_command + +from cxmanage_api.cli import get_tftp, get_nodes, run_command def mcreset_command(args): @@ -39,7 +43,7 @@ def mcreset_command(args): if not args.quiet: print 'Sending MC reset command...' - results, errors = run_command(args, nodes, 'mc_reset') + _, errors = run_command(args, nodes, 'mc_reset') if not args.quiet and not errors: print 'Command completed successfully.\n' diff --git a/cxmanage/commands/power.py b/cxmanage_api/cli/commands/power.py index b5b6015..c14de70 100644 --- a/cxmanage/commands/power.py +++ b/cxmanage_api/cli/commands/power.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: power.py """ + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,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): @@ -39,7 +42,7 @@ def power_command(args): if not args.quiet: print 'Sending power %s command...' % args.power_mode - results, errors = run_command(args, nodes, 'set_power', args.power_mode) + _, errors = run_command(args, nodes, 'set_power', args.power_mode) if not args.quiet and not errors: print 'Command completed successfully.\n' @@ -48,6 +51,7 @@ def power_command(args): def power_status_command(args): + """Executes the power status command with args.""" tftp = get_tftp(args) nodes = get_nodes(args, tftp) @@ -72,13 +76,14 @@ def power_status_command(args): def power_policy_command(args): + """Executes power policy command with args.""" tftp = get_tftp(args) nodes = get_nodes(args, tftp) if not args.quiet: print 'Setting power policy to %s...' % args.policy - results, errors = run_command(args, nodes, 'set_power_policy', + _, errors = run_command(args, nodes, 'set_power_policy', args.policy) if not args.quiet and not errors: @@ -88,6 +93,7 @@ def power_policy_command(args): def power_policy_status_command(args): + """Executes the power policy status command with args.""" tftp = get_tftp(args) nodes = get_nodes(args, tftp) diff --git a/cxmanage/commands/sensor.py b/cxmanage_api/cli/commands/sensor.py index c3fed32..97b4aba 100644 --- a/cxmanage/commands/sensor.py +++ b/cxmanage_api/cli/commands/sensor.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: sensor.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,9 +31,10 @@ # 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 +# pylint: disable=R0914 def sensor_command(args): """read sensor values from a cluster or host""" tftp = get_tftp(args) diff --git a/cxmanage_api/cli/commands/tspackage.py b/cxmanage_api/cli/commands/tspackage.py new file mode 100644 index 0000000..a5ebf15 --- /dev/null +++ b/cxmanage_api/cli/commands/tspackage.py @@ -0,0 +1,464 @@ +"""Calxeda: tspackage.py""" + + +# Copyright (c) 2012-2013, 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. + + +# +# A cxmanage command to collect information about a node and archive it. +# +# Example: +# cxmanage tspackage 10.10.10.10 +# + + +import os +import shutil +import subprocess +import sys +import tarfile +import tempfile +import time + +import pyipmi +import cxmanage_api +from cxmanage_api.cli import get_tftp, get_nodes, run_command, COMPONENTS + + +def tspackage_command(args): + """Get information pertaining to each node. + This includes: + Version info (like cxmanage info) + MAC addresses + Sensor readings + Sensor data records + Firmware info + Boot order + SELs (System Event Logs), + Depth charts + Routing Tables + + This data will be written to a set of files. Each node will get its own + file. All of these files will be archived and saved to the user's current + directory. + + Internally, this command is called from: + ~/virtual_testenv/workspace/cx_manage_util/scripts/cxmanage + + """ + tftp = get_tftp(args) + nodes = get_nodes(args, tftp) + + # Make a temporary directory to store the node information files + original_dir = os.getcwd() + temp_dir = tempfile.mkdtemp() + os.chdir(temp_dir) + tspackage_dir = "tspackage.%s" % time.strftime("%Y%m%d%H%M%S") + os.mkdir(tspackage_dir) + os.chdir(tspackage_dir) + + quiet = args.quiet + + write_client_info() + + if not quiet: + print("Getting version information...") + write_version_info(args, nodes) + + if not quiet: + print("Getting boot order...") + write_boot_order(args, nodes) + + if not quiet: + print("Getting MAC addresses...") + write_mac_addrs(args, nodes) + + if not quiet: + print("Getting sensor information...") + write_sensor_info(args, nodes) + + if not quiet: + print("Getting firmware information...") + write_fwinfo(args, nodes) + + if not quiet: + print("Getting system event logs...") + write_sel(args, nodes) + + if not quiet: + print("Getting depth charts...") + write_depth_chart(args, nodes) + + if not quiet: + print("Getting routing tables...") + write_routing_table(args, nodes) + + if not quiet: + print("Getting serial log...") + write_serial_log(args, nodes) + + if not quiet: + print("Getting crash log...") + write_crash_log(args, nodes) + + # Archive the files + archive(os.getcwd(), original_dir) + + # The original files are already archived, so we can delete them. + shutil.rmtree(temp_dir) + + +def write_client_info(): + """ Write client-side info """ + with open("client.txt", "w") as fout: + def write_command(command): + """ Safely write output from a single command to the file """ + try: + fout.write(subprocess.check_output( + command, stderr=subprocess.STDOUT, shell=True + )) + except subprocess.CalledProcessError: + pass + + fout.write("[ Operating System ]\n") + fout.write("Operating system: %s\n" % sys.platform) + write_command("lsb_release -a") + write_command("uname -a") + + fout.write("\n[ Tool versions ]\n") + fout.write("Python %s\n" % sys.version.replace("\n", "")) + fout.write("cxmanage version %s\n" % cxmanage_api.__version__) + fout.write("pyipmi version %s\n" % pyipmi.__version__) + ipmitool_path = os.environ.get('IPMITOOL_PATH', 'ipmitool') + write_command("%s -V" % ipmitool_path) + + fout.write("\n[ Python packages ]\n") + write_command("pip freeze") + + +def write_version_info(args, nodes): + """Write the version info (like cxmanage info) for each node + to their respective files. + + """ + info_results, _ = run_command(args, nodes, "get_versions") + + + for node in nodes: + lines = [ + "[ Version Info for Node %d ]" % node.node_id, + "ECME IP Address : %s" % node.ip_address + ] + + if node in info_results: + info_result = info_results[node] + lines.append( + "Hardware version : %s" % + info_result.hardware_version + ) + lines.append( + "Firmware version : %s" % + info_result.firmware_version + ) + + # Get mappings between attributes and formatted strings + components = COMPONENTS + for var, description in components: + if hasattr(info_result, var): + version = getattr(info_result, var) + lines.append("%s: %s" % (description.ljust(20), version)) + else: + lines.append("No version information could be found.") + + write_to_file(node, lines) + +def write_mac_addrs(args, nodes): + """Write the MAC addresses for each node to their respective files.""" + mac_addr_results, _ = run_command( + args, + nodes, + "get_fabric_macaddrs" + ) + + for node in nodes: + lines = [] # Lines of text to write to file + # \n is used here to give a blank line before this section + lines.append("\n[ MAC Addresses for Node %d ]" % node.node_id) + + if node in mac_addr_results: + for port in mac_addr_results[node][node.node_id]: + for mac_address in mac_addr_results[node][node.node_id][port]: + lines.append( + "Node %i, Port %i: %s" % + (node.node_id, port, mac_address) + ) + else: + lines.append("\nWARNING: No MAC addresses found!") + + write_to_file(node, lines) + +# pylint: disable=R0914 +def write_sensor_info(args, nodes): + """Write sensor information for each node to their respective files.""" + args.sensor_name = "" + + results, _ = run_command(args, nodes, "get_sensors", + args.sensor_name) + + sensors = {} + for node in nodes: + lines = [] # Lines of text to write to file + # \n is used here to give a blank line before this section + lines.append("\n[ Sensors for Node %d ]" % node.node_id) + + if node in results: + for sensor_name, sensor in results[node].iteritems(): + if not sensor_name in sensors: + sensors[sensor_name] = [] + + reading = sensor.sensor_reading.replace("(+/- 0) ", "") + try: + value = float(reading.split()[0]) + suffix = reading.lstrip("%f " % value) + sensors[sensor_name].append((node, value, suffix)) + except ValueError: + sensors[sensor_name].append((node, reading, "")) + else: + print("Could not get sensor info!") + lines.append("Could not get sensor info!") + + for sensor_name, readings in sensors.iteritems(): + for reading_node, reading, suffix in readings: + if reading_node.ip_address == node.ip_address: + left_side = "{:<18}".format(sensor_name) + right_side = ": %.2f %s" % (reading, suffix) + lines.append(left_side + right_side) + + write_to_file(node, lines) + + +def write_fwinfo(args, nodes): + """Write information about each node's firware partitions + to its respective file. + + """ + results, _ = run_command(args, nodes, "get_firmware_info") + + for node in nodes: + lines = [] # Lines of text to write to file + # \n is used here to give a blank line before this section + lines.append("\n[ Firmware Info for Node %d ]" % node.node_id) + + if node in results: + first_partition = True # The first partiton doesn't need \n + + for partition in results[node]: + if first_partition: + lines.append("Partition : %s" % partition.partition) + first_partition = False + else: + lines.append("\nPartition : %s" % partition.partition) + lines.append("Type : %s" % partition.type) + lines.append("Offset : %s" % partition.offset) + lines.append("Size : %s" % partition.size) + lines.append("Priority : %s" % partition.priority) + lines.append("Daddr : %s" % partition.daddr) + lines.append("Flags : %s" % partition.flags) + lines.append("Version : %s" % partition.version) + lines.append("In Use : %s" % partition.in_use) + else: + lines.append("Could not get firmware info!") + write_to_file(node, lines) + + +def write_boot_order(args, nodes): + """Write the boot order of each node to their respective files.""" + results, _ = run_command(args, nodes, "get_boot_order") + + for node in nodes: + lines = [] # Lines of text to write to file + # \n is used here to give a blank line before this section + lines.append("\n[ Boot Order for Node %d ]" % node.node_id) + + if node in results: + lines.append(", ".join(results[node])) + else: + lines.append("Could not get boot order!") + + write_to_file(node, lines) + + +def write_sel(args, nodes): + """Write the SEL for each node to their respective files.""" + results, _ = run_command(args, nodes, "get_sel") + + for node in nodes: + lines = [] # Lines of text to write to file + # \n is used here to give a blank line before this section + lines.append("\n[ System Event Log for Node %d ]" % node.node_id) + + try: + if node in results: + for event in results[node]: + lines.append(event) + + # pylint: disable=W0703 + except Exception as error: + lines.append("Could not get SEL! " + str(error)) + if not args.quiet: + print("Failed to get system event log for " + node.ip_address) + + write_to_file(node, lines) + + +def write_depth_chart(args, nodes): + """Write the depth chart for each node to their respective files.""" + depth_results, _ = run_command(args, nodes, "get_depth_chart") + + for node in nodes: + lines = [] # Lines of text to write to file + # \n is used here to give a blank line before this section + lines.append("\n[ Depth Chart for Node %d ]" % node.node_id) + + if node in depth_results: + depth_chart = depth_results[node] + for key in depth_chart: + subchart = depth_chart[key] + lines.append("To node " + str(key)) + + # The 'shortest' entry is one tuple, but + # the 'others' are a list. + for subkey in subchart: + if str(subkey) == "shortest": + lines.append( + " " + str(subkey) + + " : " + str(subchart[subkey]) + ) + else: + for entry in subchart[subkey]: + lines.append( + " " + str(subkey) + + " : " + str(entry) + ) + + else: + lines.append("Could not get depth chart!") + + write_to_file(node, lines) + + +def write_routing_table(args, nodes): + """Write the routing table for each node to their respective files.""" + routing_results, _ = run_command(args, nodes, "get_routing_table") + + for node in nodes: + lines = [] # Lines of text to write to file + # \n is used here to give a blank line before this section + lines.append("\n[ Routing Table for Node %d ]" % node.node_id) + + if node in routing_results: + table = routing_results[node] + for node_to in table: + lines.append(str(node_to) + " : " + str(table[node_to])) + else: + lines.append("Could not get routing table!") + + write_to_file(node, lines) + + +def write_serial_log(args, nodes): + """Write the serial log for each node""" + results, errors = run_command(args, nodes, "read_fru", 98) + for node in nodes: + lines = ["\n[ Serial log for Node %d ]" % node.node_id] + if node in results: + lines.append(results[node].strip()) + else: + lines.append(str(errors[node])) + write_to_file(node, lines) + + +def write_crash_log(args, nodes): + """Write the crash log for each node""" + results, errors = run_command(args, nodes, "read_fru", 99) + for node in nodes: + lines = ["\n[ Crash log for Node %d ]" % node.node_id] + if node in results: + lines.append(results[node].strip()) + else: + lines.append(str(errors[node])) + write_to_file(node, lines) + + +def write_to_file(node, to_write, add_newlines=True): + """Append to_write to an info file for every node in nodes. + + :param node: Node object to write about + :type node: Node object + :param to_write: Text to write to the files + :type to_write: List of strings + :param add_newlines: Whether to add newline characters before + every item in to_write. True by default. True will add newline + characters. + :type add_newlines: bool + + """ + with open("node" + str(node.node_id) + ".txt", 'a') as node_file: + if add_newlines: + node_file.write("%s\n" % "\n".join(to_write)) + else: + node_file.write("".join(to_write)) + + +def archive(directory_to_archive, destination): + """Creates a .tar containing everything in the directory_to_archive. + The .tar is saved to destination with the same name as the original + directory_to_archive, but with .tar appended. + + :param directory_to_archive: A path to the directory to be archived. + :type directory_to_archive: string + + :param destination: A path to the location the .tar should be saved + :type destination: string + + """ + os.chdir(os.path.dirname(directory_to_archive)) + + tar_name = os.path.basename(directory_to_archive) + ".tar" + tar_name = os.path.join(destination, tar_name) + + with tarfile.open(tar_name, "w") as tar: + tar.add(os.path.basename(directory_to_archive)) + + print( + "Finished! One archive created:\n" + + os.path.join(destination, tar_name) + ) diff --git a/cxmanage_api/crc32.py b/cxmanage_api/crc32.py index aca7838..aa85179 100644 --- a/cxmanage_api/crc32.py +++ b/cxmanage_api/crc32.py @@ -1,4 +1,4 @@ -# Copyright (c) 2012, Calxeda Inc. +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # diff --git a/cxmanage_api/cx_exceptions.py b/cxmanage_api/cx_exceptions.py index 410b5d7..5e0c931 100644 --- a/cxmanage_api/cx_exceptions.py +++ b/cxmanage_api/cx_exceptions.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: cx_exceptions.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,12 +31,46 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. -"""Defines the custom exceptions used by the cxmanage_api project.""" +# +# We expose these here so a user does not have to import from pyipmi or tftpy. +# +# pylint: disable=W0611 +# from pyipmi import IpmiError + 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. @@ -109,31 +146,6 @@ class NoSensorError(Exception): return self.msg -class NoFirmwareInfoError(Exception): - """Raised when the firmware info cannot be obtained from a node. - - >>> from cxmanage_api.cx_exceptions import NoFirmwareInfoError - >>> raise NoFirmwareInfoError('My custom exception text!') - Traceback (most recent call last): - File "<stdin>", line 1, in <module> - cxmanage_api.cx_exceptions.NoFirmwareInfoError: My custom exception text! - - :param msg: Exceptions message and details to return to the user. - :type msg: string - :raised: When the firmware info cannot be obtained from a node. - - """ - - def __init__(self, msg): - """Default constructor for the NoFirmwareInfoError class.""" - super(NoFirmwareInfoError, self).__init__() - self.msg = msg - - def __str__(self): - """String representation of this Exception class.""" - return self.msg - - class SocmanVersionError(Exception): """Raised when there is an error with the users socman version. @@ -284,26 +296,25 @@ class InvalidImageError(Exception): return self.msg -class UnknownBootCmdError(Exception): - """Raised when the boot command is not: run bootcmd_pxe, run bootcmd_sata, - run bootcmd_mmc, setenv bootdevice, or reset. +class UbootenvError(Exception): + """Raised when the UbootEnv class fails to interpret the ubootenv + environment variables. - >>> from cxmanage_api.cx_exceptions import UnknownBootCmdError - >>> raise UnknownBootCmdError('My custom exception text!') + >>> from cxmanage_api.cx_exceptions import UbootenvError + >>> raise UbootenvError('My custom exception text!') Traceback (most recent call last): File "<stdin>", line 1, in <module> - cxmanage_api.cx_exceptions.UnknownBootCmdError: My custom exception text! + cxmanage_api.cx_exceptions.UbootenvError: My custom exception text! :param msg: Exceptions message and details to return to the user. :type msg: string - :raised: When the boot command is not: run bootcmd_pxe, run bootcmd_sata, - run bootcmd_mmc, setenv bootdevice, or reset. + :raised: When ubootenv settings are unrecognizable. """ def __init__(self, msg): - """Default constructor for the UnknownBootCmdError class.""" - super(UnknownBootCmdError, self).__init__() + """Default constructor for the UbootenvError class.""" + super(UbootenvError, self).__init__() self.msg = msg def __str__(self): @@ -330,6 +341,7 @@ class CommandFailedError(Exception): def __init__(self, results, errors): """Default constructor for the CommandFailedError class.""" + super(CommandFailedError, self).__init__() self.results = results self.errors = errors @@ -390,4 +402,8 @@ class IPDiscoveryError(Exception): return self.msg +class ParseError(Exception): + """Raised when there's an error parsing some output""" + pass + # End of file: exceptions.py diff --git a/cxmanage_api/docs/generate_api_rst.py b/cxmanage_api/docs/generate_api_rst.py index 1e5a901..776dabe 100755 --- a/cxmanage_api/docs/generate_api_rst.py +++ b/cxmanage_api/docs/generate_api_rst.py @@ -2,7 +2,7 @@ :author: Eric Vasquez :contact: eric.vasquez@calxeda.com -:copyright: (c) 2012, Calxeda Inc. +:copyright: (c) 2012-2013, Calxeda Inc. """ @@ -62,7 +62,7 @@ def get_source(source_dir): source = {API_NAME : {}} paths = glob.glob(os.path.join(source_dir, '*.py')) for path in paths: - f_path, f_ext = os.path.splitext(path) + f_path, _ = os.path.splitext(path) f_name = f_path.split(source_dir)[1] if (not f_name in BLACKLIST): if TITLES.has_key(f_name): diff --git a/cxmanage_api/docs/source/conf.py b/cxmanage_api/docs/source/conf.py index 6ac0b01..e3b47cf 100644 --- a/cxmanage_api/docs/source/conf.py +++ b/cxmanage_api/docs/source/conf.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# pylint: skip-file # # Cxmanage Python API documentation build configuration file, created by # sphinx-quickstart on Fri Dec 07 16:31:44 2012. diff --git a/cxmanage_api/docs/source/index.rst b/cxmanage_api/docs/source/index.rst index 1630ae6..3d91f4e 100644 --- a/cxmanage_api/docs/source/index.rst +++ b/cxmanage_api/docs/source/index.rst @@ -172,6 +172,7 @@ API Docs & Code Examples SIMG <simg> U-Boot Environment <ubootenv> IP Retriever <ip_retriever> + Loggers <loggers> ``Code Examples`` diff --git a/cxmanage_api/fabric.py b/cxmanage_api/fabric.py index 34f435e..7582aee 100644 --- a/cxmanage_api/fabric.py +++ b/cxmanage_api/fabric.py @@ -1,4 +1,8 @@ -# Copyright (c) 2012, Calxeda Inc. +# pylint: disable=C0302 +"""Calxeda: fabric.py """ + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,12 +32,16 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. +import time + from cxmanage_api.tasks import DEFAULT_TASK_QUEUE from cxmanage_api.tftp import InternalTftp from cxmanage_api.node import Node as NODE -from cxmanage_api.cx_exceptions import CommandFailedError +from cxmanage_api.cx_exceptions import CommandFailedError, TimeoutError, \ + IpmiError, TftpException +# pylint: disable=R0902,R0903, R0904 class Fabric(object): """ The Fabric class provides management of multiple nodes. @@ -56,6 +64,54 @@ class Fabric(object): :type node: `Node <node.html>`_ """ + class CompositeBMC(object): + """ Composite BMC object. Provides a mechanism to run BMC + commands in parallel across all nodes. + """ + + def __init__(self, fabric): + self.fabric = fabric + + def __getattr__(self, name): + """ If the underlying BMCs have a method by this name, then return + a callable function that does it in parallel across all nodes. + """ + nodes = self.fabric.nodes + task_queue = self.fabric.task_queue + + for node in nodes.values(): + if ((not hasattr(node.bmc, name)) or + (not hasattr(getattr(node.bmc, name), "__call__"))): + raise AttributeError( + "'CompositeBMC' object has no attribute '%s'" + % name + ) + + def function(*args, **kwargs): + """ Run the named BMC command in parallel across all nodes. """ + tasks = {} + for node_id, node in nodes.iteritems(): + tasks[node_id] = task_queue.put( + getattr(node.bmc, name), + *args, + **kwargs + ) + + results = {} + errors = {} + for node_id, task in tasks.items(): + task.join() + if task.status == "Completed": + results[node_id] = task.result + else: + errors[node_id] = task.error + if errors: + raise CommandFailedError(results, errors) + return results + + return function + + # pylint: disable=R0913 def __init__(self, ip_address, username="admin", password="admin", tftp=None, ecme_tftp_port=5001, task_queue=None, verbose=False, node=None): @@ -68,6 +124,7 @@ class Fabric(object): self.task_queue = task_queue self.verbose = verbose self.node = node + self.cbmc = Fabric.CompositeBMC(self) self._nodes = {} @@ -77,9 +134,6 @@ class Fabric(object): if (not self.task_queue): self.task_queue = DEFAULT_TASK_QUEUE - if (not self._tftp): - self._tftp = InternalTftp() - def __eq__(self, other): """__eq__() override.""" return (isinstance(other, Fabric) and self.nodes == other.nodes) @@ -104,6 +158,9 @@ class Fabric(object): :rtype: `Tftp <tftp.html>`_ """ + if (not self._tftp): + self._tftp = InternalTftp.default() + return self._tftp @tftp.setter @@ -137,7 +194,8 @@ class Fabric(object): """ if not self._nodes: - self._discover_nodes(self.ip_address) + self.refresh() + return self._nodes @property @@ -154,6 +212,47 @@ class Fabric(object): """ return self.nodes[0] + def refresh(self, wait=False, timeout=600): + """Gets the nodes of this fabric by pulling IP info from a BMC.""" + def get_nodes(): + """Returns a dictionary of nodes reported by the primary node IP""" + new_nodes = {} + node = self.node( + ip_address=self.ip_address, username=self.username, + password=self.password, tftp=self.tftp, + ecme_tftp_port=self.ecme_tftp_port, verbose=self.verbose + ) + ipinfo = node.get_fabric_ipinfo() + for node_id, node_address in ipinfo.iteritems(): + new_nodes[node_id] = self.node( + ip_address=node_address, username=self.username, + password=self.password, tftp=self.tftp, + ecme_tftp_port=self.ecme_tftp_port, + verbose=self.verbose + ) + new_nodes[node_id].node_id = node_id + return new_nodes + + initial_node_count = len(self._nodes) + self._nodes = {} + + if wait: + deadline = time.time() + timeout + while time.time() < deadline: + try: + self._nodes = get_nodes() + if len(self._nodes) >= initial_node_count: + break + except (IpmiError, TftpException): + pass + else: + raise TimeoutError( + "Fabric refresh timed out. Rediscovered %i of %i nodes" + % (len(self._nodes), initial_node_count) + ) + else: + self._nodes = get_nodes() + def get_mac_addresses(self): """Gets MAC addresses from all nodes. @@ -165,26 +264,20 @@ class Fabric(object): 3: ['fc:2f:40:88:b3:6c', 'fc:2f:40:88:b3:6d', 'fc:2f:40:88:b3:6e'] } - :param async: Flag that determines if the command result (dictionary) - is returned or a Task object (can get status, etc.). - :type async: boolean - :return: The MAC addresses for each node. :rtype: dictionary """ return self.primary_node.get_fabric_macaddrs() - def get_uplink_info(self): + def get_uplink_info(self, async=False): """Gets the fabric uplink info. >>> fabric.get_uplink_info() - { - 0: {0: 0, 1: 0, 2: 0} - 1: {0: 0, 1: 0, 2: 0} - 2: {0: 0, 1: 0, 2: 0} - 3: {0: 0, 1: 0, 2: 0} - } + {0: 'Node 0: eth0 0, eth1 0, mgmt 0', + 1: 'Node 1: eth0 0, eth1 0, mgmt 0', + 2: 'Node 2: eth0 0, eth1 0, mgmt 0', + 3: 'Node 3: eth0 0, eth1 0, mgmt 0'} :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). @@ -194,7 +287,23 @@ class Fabric(object): :rtype: dictionary """ - return self.primary_node.get_fabric_uplink_info() + return self._run_on_all_nodes(async, "get_uplink_info") + + def get_uplink_speed(self, async=False): + """Gets the uplink speed of every node in the fabric. + + >>> fabric.get_uplink_speed() + {0: 1, 1: 0, 2: 0, 3: 0} + + :param async: Flag that determines if the command result (dictionary) + is returned or a Task object (can get status, etc.). + :type async: boolean + + :return: The uplink info for each node. + :rtype: dictionary + + """ + return self._run_on_all_nodes(async, "get_uplink_speed") def get_power(self, async=False): """Returns the power status for all nodes. @@ -212,7 +321,7 @@ class Fabric(object): """ return self._run_on_all_nodes(async, "get_power") - def set_power(self, mode, async=False): + def set_power(self, mode, async=False, ignore_existing_state=False): """Send an IPMI power command to all nodes. >>> # On ... @@ -228,9 +337,13 @@ class Fabric(object): :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean + :param ignore_existing_state: Flag that allows the caller to only try + to turn on or off nodes that are not + turned on or off, respectively. + :type ignore_existing_state: boolean """ - self._run_on_all_nodes(async, "set_power", mode) + self._run_on_all_nodes(async, "set_power", mode, ignore_existing_state) def get_power_policy(self, async=False): """Gets the power policy from all nodes. @@ -476,6 +589,41 @@ class Fabric(object): """ return self._run_on_all_nodes(async, "get_boot_order") + def set_pxe_interface(self, interface, async=False): + """Sets the pxe interface on all nodes. + + >>> fabric.set_pxe_interface(interface='eth0') + + :param interface: Inteface for pxe requests + :type interface: string + :param async: Flag that determines if the command result (dictionary) + is returned or a Command object (can get status, etc.). + :type async: boolean + + """ + self._run_on_all_nodes(async, "set_pxe_interface", interface) + + def get_pxe_interface(self, async=False): + """Gets the pxe interface from all nodes. + + >>> fabric.get_pxe_interface() + { + 0: 'eth0', + 1: 'eth0', + 2: 'eth0', + 3: 'eth0' + } + + :param async: Flag that determines if the command result (dictionary) + is returned or a Command object (can get status, etc.). + :type async: boolean + + :returns: The boot order of each node on this fabric. + :rtype: dictionary or `Task <tasks.html>`__ + + """ + return self._run_on_all_nodes(async, "get_pxe_interface") + def get_versions(self, async=False): """Gets the version info from all nodes. @@ -488,7 +636,8 @@ class Fabric(object): } .. seealso:: - `Node.get_versions() <node.html#cxmanage_api.node.Node.get_versions>`_ + `Node.get_versions() \ +<node.html#cxmanage_api.node.Node.get_versions>`_ :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). @@ -527,7 +676,8 @@ class Fabric(object): } .. seealso:: - `Node.get_versions_dict() <node.html#cxmanage_api.node.Node.get_versions_dict>`_ + `Node.get_versions_dict() \ +<node.html#cxmanage_api.node.Node.get_versions_dict>`_ :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). @@ -586,6 +736,7 @@ class Fabric(object): """ return self._run_on_all_nodes(async, "get_ubootenv") + # pylint: disable=R0913 def get_server_ip(self, interface=None, ipv6=False, user="user1", password="1Password", aggressive=False, async=False): """Get the server IP address from all nodes. The nodes must be powered @@ -716,7 +867,7 @@ class Fabric(object): :param nodeid: Node id from which the macaddr is to be remove :type nodeid: integer - :param iface: interface on the node from which the macaddr is to be removed + :param iface: interface on the node which the macaddr is to be removed :type iface: integer :param macaddr: mac address to be removed :type macaddr: string @@ -725,6 +876,51 @@ class Fabric(object): self.primary_node.bmc.fabric_rm_macaddr(nodeid=nodeid, iface=iface, macaddr=macaddr) + def set_macaddr_base(self, macaddr): + """ Set a base MAC address for a custom range. + + >>> fabric.set_macaddr_base("66:55:44:33:22:11") + + :param macaddr: mac address base to use + :type macaddr: string + + """ + self.primary_node.bmc.fabric_config_set_macaddr_base(macaddr=macaddr) + + def get_macaddr_base(self): + """ Get the base MAC address for custom ranges. + + >>> fabric.get_macaddr_base() + '08:00:00:00:08:5c' + + :return: mac address base + :rtype: string + """ + return self.primary_node.bmc.fabric_config_get_macaddr_base() + + def set_macaddr_mask(self, mask): + """ Set MAC address mask for a custom range. + + >>> fabric.set_macaddr_mask("ff:ff:ff:ff:ff:00") + + :param macaddr: mac address mask to use + :type macaddr: string + + """ + self.primary_node.bmc.fabric_config_set_macaddr_mask(mask=mask) + + def get_macaddr_mask(self): + """ Get the MAC address mask for custom ranges. + + >>> fabric.get_macaddr_mask() + '08:00:00:00:08:5c' + + :return: mac address mask + :rtype: string + + """ + return self.primary_node.bmc.fabric_config_get_macaddr_mask() + def get_linkspeed_policy(self): """Get the global linkspeed policy for the fabric. In the partition world this means the linkspeed for Configuration 0, Partition 0, @@ -797,7 +993,7 @@ class Fabric(object): def set_uplink(self, uplink=0, iface=0): """Set the uplink for an interface to xmit a packet out of the cluster. - >>> fabric.set_uplink(0,0) + >>> fabric.set_uplink(uplink=0,iface=0) :param uplink: The uplink to set. :type uplink: integer @@ -811,6 +1007,33 @@ class Fabric(object): def get_link_stats(self, link=0, async=False): """Get the link_stats for each node in the fabric. + >>> fabric.get_link_stats() + {0: {'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': '0x3e89f', + 'FS_LC0_RERRSCNT': '0x0', + 'FS_LC0_RMCSCNT': '0x174b9bf', + 'FS_LC0_RPKTSCNT': '0x0', + 'FS_LC0_RUCSCNT': '0x43e9b', + 'FS_LC0_SC_STAT': '0x0', + 'FS_LC0_STATE': '0x1033', + 'FS_LC0_TDRPSCNT': '0x0', + 'FS_LC0_TPKTSCNT': '0x1'}, + }} + >>> # + >>> # Output trimmed for brevity ... + >>> # The data shown for node 0 is the same type of data for each + >>> # node in the fabric. + >>> # + :param link: The link to get stats for (0-4). :type link: integer @@ -827,6 +1050,9 @@ class Fabric(object): def get_linkmap(self, async=False): """Get the linkmap for each node in the fabric. + >>> fabric.get_linkmap() + {0: {1: 2, 3: 1, 4: 3}, 1: {3: 0}, 2: {3: 0, 4: 3}, 3: {3: 0, 4: 2}} + :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean @@ -840,6 +1066,12 @@ class Fabric(object): def get_routing_table(self, async=False): """Get the routing_table for the fabric. + >>> fabric.get_routing_table() + {0: {1: [0, 0, 0, 3, 0], 2: [0, 3, 0, 0, 2], 3: [0, 2, 0, 0, 3]}, + 1: {0: [0, 0, 0, 3, 0], 2: [0, 0, 0, 2, 0], 3: [0, 0, 0, 2, 0]}, + 2: {0: [0, 0, 0, 3, 2], 1: [0, 0, 0, 2, 0], 3: [0, 0, 0, 2, 3]}, + 3: {0: [0, 0, 0, 3, 2], 1: [0, 0, 0, 2, 0], 2: [0, 0, 0, 2, 3]}} + :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean @@ -853,6 +1085,20 @@ class Fabric(object): def get_depth_chart(self, async=False): """Get the depth_chart for the fabric. + >>> fabric.get_depth_chart() + {0: {1: {'shortest': (0, 0)}, + 2: {'others': [(3, 1)], 'shortest': (0, 0)}, + 3: {'others': [(2, 1)], 'shortest': (0, 0)}}, + 1: {0: {'shortest': (1, 0)}, + 2: {'others': [(3, 2)], 'shortest': (0, 1)}, + 3: {'others': [(2, 2)], 'shortest': (0, 1)}}, + 2: {0: {'others': [(3, 1)], 'shortest': (2, 0)}, + 1: {'shortest': (0, 1)}, + 3: {'others': [(0, 1)], 'shortest': (2, 0)}}, + 3: {0: {'others': [(2, 1)], 'shortest': (3, 0)}, + 1: {'shortest': (0, 1)}, + 2: {'others': [(0, 1)], 'shortest': (3, 0)}}} + :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean @@ -863,11 +1109,12 @@ class Fabric(object): """ return self._run_on_all_nodes(async, "get_depth_chart") - def _run_on_all_nodes(self, async, name, *args): + def _run_on_all_nodes(self, async, name, *args, **kwargs): """Start a command on all nodes.""" tasks = {} for node_id, node in self.nodes.iteritems(): - tasks[node_id] = self.task_queue.put(getattr(node, name), *args) + tasks[node_id] = self.task_queue.put(getattr(node, name), *args, + **kwargs) if async: return tasks @@ -884,21 +1131,5 @@ class Fabric(object): raise CommandFailedError(results, errors) return results - def _discover_nodes(self, ip_address, username="admin", password="admin"): - """Gets the nodes of this fabric by pulling IP info from a BMC.""" - node = self.node(ip_address=ip_address, username=username, - password=password, tftp=self.tftp, - ecme_tftp_port=self.ecme_tftp_port, - verbose=self.verbose) - ipinfo = node.get_fabric_ipinfo() - for node_id, node_address in ipinfo.iteritems(): - self._nodes[node_id] = self.node(ip_address=node_address, - username=username, - password=password, - tftp=self.tftp, - ecme_tftp_port=self.ecme_tftp_port, - verbose=self.verbose) - self._nodes[node_id].node_id = node_id - # End of file: ./fabric.py diff --git a/cxmanage_api/firmware_package.py b/cxmanage_api/firmware_package.py index 433b596..7f8e645 100644 --- a/cxmanage_api/firmware_package.py +++ b/cxmanage_api/firmware_package.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: firmware_package.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -34,11 +37,13 @@ import tarfile import ConfigParser import pkg_resources +import cxmanage_api from cxmanage_api import temp_dir from cxmanage_api.image import Image -class FirmwarePackage: +# pylint: disable=R0903 +class FirmwarePackage(object): """A firmware update package contains multiple images & version information. .. note:: @@ -54,6 +59,7 @@ class FirmwarePackage: """ + # pylint: disable=R0912 def __init__(self, filename=None): """Default constructor for the FirmwarePackage class.""" self.images = [] @@ -76,15 +82,16 @@ class FirmwarePackage: % os.path.basename(filename)) if "package" in config.sections(): - cxmanage_ver = config.get("package", - "required_cxmanage_version") - try: - pkg_resources.require("cxmanage>=%s" % cxmanage_ver) - except pkg_resources.VersionConflict: + required_cxmanage_version = config.get( + "package", "required_cxmanage_version" + ) + if (pkg_resources.parse_version(cxmanage_api.__version__) < + pkg_resources.parse_version(required_cxmanage_version)): # @todo: CxmanageVersionError? raise ValueError( - "%s requires cxmanage version %s or later." - % (filename, cxmanage_ver)) + "%s requires cxmanage version %s or later." + % (filename, required_cxmanage_version) + ) if config.has_option("package", "required_socman_version"): self.required_socman_version = config.get("package", @@ -117,6 +124,9 @@ class FirmwarePackage: self.images.append(Image(filename, image_type, simg, daddr, skip_crc32, version)) + def __str__(self): + return self.version + def save_package(self, filename): """Save all images as a firmware package. diff --git a/cxmanage_api/image.py b/cxmanage_api/image.py index 23642c4..8f3011a 100644 --- a/cxmanage_api/image.py +++ b/cxmanage_api/image.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: image.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -38,7 +41,7 @@ from cxmanage_api.simg import valid_simg, get_simg_contents from cxmanage_api.cx_exceptions import InvalidImageError -class Image: +class Image(object): """An Image consists of: an image type, a filename, and SIMG header info. >>> from cxmanage_api.image import Image @@ -62,6 +65,7 @@ class Image: """ + # pylint: disable=R0913 def __init__(self, filename, image_type, simg=None, daddr=None, skip_crc32=False, version=None): """Default constructor for the Image class.""" @@ -114,8 +118,8 @@ class Image: skip_crc32=self.skip_crc32, align=align, version=self.version) filename = temp_file() - with open(filename, "w") as f: - f.write(simg) + with open(filename, "w") as file_: + file_.write(simg) # Make sure the simg was built correctly if (not valid_simg(open(filename).read())): @@ -152,7 +156,7 @@ class Image: :rtype: boolean """ - if (self.type == "SOC_ELF"): + if (self.type == "SOC_ELF" and not self.simg): try: file_process = subprocess.Popen(["file", self.filename], stdout=subprocess.PIPE) diff --git a/cxmanage_api/ip_retriever.py b/cxmanage_api/ip_retriever.py index 411465b..ef443cb 100644 --- a/cxmanage_api/ip_retriever.py +++ b/cxmanage_api/ip_retriever.py @@ -1,6 +1,7 @@ -#!/usr/bin/env python +"""Calxeda: ip_retriever.py""" -# Copyright (c) 2012, Calxeda Inc. + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -30,6 +31,7 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. + import sys import re import json @@ -45,9 +47,10 @@ from pyipmi.server import Server from pyipmi.bmc import LanBMC +# pylint: disable=R0902 class IPRetriever(threading.Thread): - """The IPRetriever class takes an ECME address and when run will - connect to the Linux Server from the ECME over SOL and use + """The IPRetriever class takes an ECME address and when run will + connect to the Linux Server from the ECME over SOL and use ifconfig to determine the IP address. """ verbosity = None @@ -55,7 +58,7 @@ class IPRetriever(threading.Thread): retry = None timeout = None interface = None - + ecme_ip = None ecme_user = None ecme_password = None @@ -63,14 +66,14 @@ class IPRetriever(threading.Thread): server_ip = None server_user = None server_password = None - + def __init__(self, ecme_ip, aggressive=False, verbosity=0, **kwargs): """Initializes the IPRetriever class. The IPRetriever needs the only the first node to know where to start. """ super(IPRetriever, self).__init__() self.daemon = True - + if hasattr(ecme_ip, 'ip_address'): self.ecme_ip = ecme_ip.ip_address else: @@ -78,7 +81,7 @@ class IPRetriever(threading.Thread): self.aggressive = aggressive self.verbosity = verbosity - + # Everything here is optional self.timeout = kwargs.get('timeout', 120) self.retry = kwargs.get('retry', 0) @@ -95,20 +98,20 @@ class IPRetriever(threading.Thread): self._ip_pattern = kwargs['_ip_pattern'] else: - self.set_interface(kwargs.get('interface', None), + self.set_interface(kwargs.get('interface', None), kwargs.get('ipv6', False)) if 'bmc' in kwargs: self._bmc = kwargs['bmc'] else: - self._bmc = make_bmc(LanBMC, verbose=(self.verbosity>1), - hostname=self.ecme_ip, + self._bmc = make_bmc(LanBMC, verbose=(self.verbosity > 1), + hostname=self.ecme_ip, username=self.ecme_user, password=self.ecme_password) if 'config_path' in kwargs: self.read_config(kwargs['config_path']) - + def set_interface(self, interface=None, ipv6=False): @@ -119,11 +122,13 @@ class IPRetriever(threading.Thread): self.interface = interface if not ipv6: - self._ip_pattern = re.compile('\d+\.'*3 + '\d+') + self._ip_pattern = re.compile(r'\d+\.' * 3 + r'\d+') self._inet_pattern = re.compile('inet addr:(%s)' % self._ip_pattern.pattern) else: - self._ip_pattern = re.compile('[0-9a-fA-F:]*:'*2 + '[0-9a-fA-F:]+') + self._ip_pattern = re.compile( + '[0-9a-fA-F:]*:' * 2 + '[0-9a-fA-F:]+' + ) self._inet_pattern = re.compile('inet6 addr: ?(%s)' % self._ip_pattern.pattern) @@ -137,14 +142,14 @@ class IPRetriever(threading.Thread): def run(self): - """Attempts to finds the server IP address associated with the + """Attempts to finds the server IP address associated with the ECME IP. If successful, server_ip will contain the IP address. """ if self.server_ip is not None: self._log('Using stored IP %s' % self.server_ip) return - for attempt in range(self.retry + 1): + for _ in range(self.retry + 1): self.server_ip = self.sol_try_command(self.sol_find_ip) if self.server_ip is not None: @@ -160,7 +165,7 @@ class IPRetriever(threading.Thread): """ server = Server(self._bmc) - if cycle: + if cycle: self._log('Powering server off') server.power_off() sleep(5) @@ -201,15 +206,16 @@ class IPRetriever(threading.Thread): raise IPDiscoveryError('Could not find interface %s' % self.interface) - else: # Failed to find interface. Returning None + else: # Failed to find interface. Returning None return None - + + # pylint: disable=R0912, R0915 def sol_try_command(self, command): """Connects to the server over a SOL connection. Attempts to run the given command on the server without knowing the state of the server. The command must return None if - it fails. If aggresive is True, then the server may be + it fails. If aggresive is True, then the server may be restarted or power cycled to try and reset the state. """ server = Server(self._bmc) @@ -303,7 +309,7 @@ class IPRetriever(threading.Thread): else: # Assume where are at a prompt and able to run the command value = command(session) - + if value is not None: self._bmc.deactivate_payload() return value diff --git a/cxmanage_api/loggers.py b/cxmanage_api/loggers.py new file mode 100644 index 0000000..1e4f4ab --- /dev/null +++ b/cxmanage_api/loggers.py @@ -0,0 +1,398 @@ +# Copyright (c) 2012-2013, 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. + + +"""Loggers is a set of Logging classes used to capture output. + +The most commonly used loggers are StandardOutLogger and FileLogger. +Additionally, these loggers can be combined to write output to more than one +target. + +""" + + +import os +import datetime +import traceback + + +# +# Log Level Definitions +# +LL_DEBUG = 4 +LL_INFO = 3 +LL_WARN = 2 +LL_ERROR = 1 +LL_NONE = 0 +DEFAULT_LL = LL_INFO + + +class Logger(object): + """Base class for all loggers. + + To create a custom logger, inherit from this class, and implement + the write() method so that it writes message in the appropriate manner. + + >>> # To use this class for inheritance ... + >>> from cx_automation.loggers import Logger + >>> + + :param log_level: Verbosity level of logging for this logger. + :type log_level: integer + :param time_stamp: Flag to determine toggle time_stamping each log entry. + :type time_stamp: boolean + :param component: Component tag for the log entry. + :type component: string + + .. note:: + * This class is not intended to be used as a logger itself. + * Only the **write()** method needs to be implemeneted for your custom + logger. + * Log Levels: DEBUG=4, INFO=3, WARN=2, ERROR=1, NONE=0 + * You can turn OFF entry time_stamping by initializing a logger with: + **time_stamp=False** + + """ + + def __init__(self, log_level=DEFAULT_LL, time_stamp=True, component=None): + """Default constructor for the Logger class.""" + self.log_level = log_level + self.time_stamp = time_stamp + + if (component): + self.component = '| ' + component + else: + self.component = '' + + def _get_log(self, msg, level_tag): + """Used internally to create an appropriate log message string. + + :param msg: The message to write. + :type msg: string + :param level_tag: The log level string, e.g. INFO, DEBUG, WARN, etc. + :type level_tag: string + + """ + lines = msg.split('\n') + result = [] + for line in lines: + if (self.time_stamp): + ts_now = str(datetime.datetime.now()) + result.append( + '%s %s | %s : %s' % + (ts_now, self.component, level_tag, line) + ) + else: + result.append( + '%s %s : %s' % + (self.component, level_tag, line) + ) + + return '\n'.join(result) + + # pylint: disable=R0201 + def write(self, message): + """Writes a log message. + + .. warning:: + * This method is to be intentionally overridden. + * Implemented by subclasses. + + :param message: The message to write.. + :type message: string + + :raises NotImplementedError: If write() is not overridden. + + """ + del message # For function signature only! + raise NotImplementedError + + def debug(self, message): + """Log a message at DEBUG level. LL_DEBUG = 4 + + >>> logger.debug('This is debug.') + 2012-12-19 11:13:04.329046 | DEBUG | This is debug. + + :param message: The message to write. + :type message: string + + """ + if (self.log_level >= LL_DEBUG): + self.write(self._get_log(message, "DEBUG")) + + def info(self, message): + """Log a message at the INFO level. LL_INFO = 3 + + >>> logger.info('This is informational.') + 2012-12-19 11:11:47.225859 | INFO | This is informational. + + :param message: The message to write. + :type message: string + + """ + if (self.log_level >= LL_INFO): + self.write(self._get_log(msg=message, level_tag="INFO")) + + def warn(self, message): + """Log a message at WARN level. LL_WARN = 2 + + >>> logger.warn('This is a warning') + 2012-12-19 11:11:12.257814 | WARN | This is a warning + + :param message: The message to write. + :type message: string + + """ + if (self.log_level >= LL_WARN): + self.write(self._get_log(msg=message, level_tag="WARN")) + + def error(self, message): + """Log a message at ERROR level. LL_ERROR = 1 + + >>> logger.error('This is an error.') + 2012-12-19 11:14:11.352735 | ERROR | This is an error. + + :param message: The message to write. + :type message: string + + """ + if (self.log_level >= LL_ERROR): + self.write(self._get_log(msg=message, level_tag="ERROR")) + + +class StandardOutLogger(Logger): + """A Logger class that writes to Standard Out (stdout). + + Only the write method has to be implemented. + + >>> # Typical instantiation ... + >>> from cx_automation.loggers import StandardOutLogger + >>> logger = StandardOutLogger() + + + :param log_level: Level of logging for this logger. + :type log_level: integer + :param time_stamp: Flag to determine toggle time_stamping each log entry. + :type time_stamp: boolean + + """ + + def __init__(self, log_level=DEFAULT_LL, time_stamp=True, component=None): + """Default constructor for a StandardOutLogger.""" + self.log_level = log_level + self.time_stamp = time_stamp + self.component = component + super(StandardOutLogger, self).__init__( + log_level=self.log_level, + time_stamp=self.time_stamp, + component=self.component + ) + + def write(self, message): + """Writes a log message to standard out. + + >>> # It simply prints ... + >>> logger.write('This function is called by the Base Class') + This function is called by the Base Class + >>> + + :param message: The message to write. + :type message: string + + """ + print message + + +class FileLogger(Logger): + """A logger that writes to a file. + + >>> # Typical instantiation ... + >>> flogger = FileLogger(abs_path='/home/logfile.out') + + :param log_level: Level of logging for this logger. + :type log_level: integer + :param time_stamp: Flag to determine toggle time_stamping each log entry. + :type time_stamp: boolean + :param name: Name of this logger. + :type name: string + + """ + + def __init__(self, abs_path, time_stamp=True, component=None, + log_level=DEFAULT_LL): + """Default constructor for the FileLogger class.""" + super(FileLogger, self).__init__( + log_level=log_level, + time_stamp=time_stamp, + component=component + ) + self.path = abs_path + try: + if not (os.path.exists(self.path)): + file(self.path, 'w').close() + + except Exception: + raise + + def write(self, message): + """Writes a log message to a log file. + + :param message: The message to write. + :type message: string + + """ + try: + old_umask = os.umask(0000) + with open(self.path, 'a') as file_d: + file_d.write(message + "\n") + file_d.close() + + except Exception: + self.error(traceback.format_exc()) + raise + + finally: + os.umask(old_umask) + if (file_d): + file_d.close() + + +class CompositeLogger(object): + """Takes a list of loggers and writes the same output to them all. + + >>> from cx_automation.loggers import StandardOutLogger, FileLogger + >>> # Let's say you want to log to a file while also seeing the output. + >>> # Create a StandardOutLogger to 'see' output. + >>> slogger = StandarOutLogger(...) + >>> # Create a FileLogger to log to a file. + >>> flogger = FileLogger(...) + >>> from cx_automation.loggers import CompositeLogger + >>> # Create a composite logger and you can log to both simultaneously! + >>> logger = CompositeLogger(loggers=[slogger, flogger]) + + :param loggers: A list of loggers to output to + :type loggers: list + :param log_level: The level to log at. DEFAULT: LL_INFO + :type log_level: integer + + """ + + def __init__(self, loggers, log_level=DEFAULT_LL): + """Default constructor for the CompositeLogger class.""" + self.loggers = loggers + self._log_level = log_level + # + # Set the log level to the same for all loggers ... + # + for logger in self.loggers: + logger.log_level = log_level + + @property + def log_level(self): + """Returns the log_level for ALL loggers. + + >>> logger.log_level + >>> 3 + + :returns: The log_level for ALL loggers. + :rtype: integer + + """ + return self._log_level + + @log_level.setter + def log_level(self, value): + """Sets the log_level for ALL loggers. + + :param value: The value to set the log_level to. + :type value: integer + + """ + self._log_level = value + if (not self._log_level): + return + + for logger in self.loggers: + logger.log_level = value + + def info(self, message): + """Loga a message at the INFO level: LL_INFO = 3 for all loggers. + + >>> logger.info('This is informational.') + 2012-12-19 11:37:17.462879 | INFO | This is informational. + + :param message: The message to write. + :type message: string + + """ + for logger in self.loggers: + logger.info(message) + + def warn(self, message): + """Log a message at WARN level: LL_WARN = 2 for all loggers. + + >>> logger.warn('This is a warning.') + 2012-12-19 11:37:50.614862 | WARN | This is a warning. + + :param message: The message to write. + :type message: string + + """ + for logger in self.loggers: + logger.warn(message) + + def error(self, message): + """Log a message at ERROR level. LL_ERROR = 1 for all loggers. + + >>> logger.error('This is an ERROR!') + 2012-12-19 11:41:18.181123 | ERROR | This is an ERROR! + + :param message: The message to write. + :type message: string + + """ + for logger in self.loggers: + logger.error(message) + + def debug(self, message): + """ + Log a message at DEBUG level. LL_DEBUG = 4 for all loggers. + + >>> logger.debug('This is a DEBUG log entry. Message goes here') + + :param message: The message to write. + :type message: string + + """ + for logger in self.loggers: + logger.debug(message) + + +# End of File: cx_automation/utilites/loggers.py 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 diff --git a/cxmanage_api/simg.py b/cxmanage_api/simg.py index 6ae8bf8..1870691 100644 --- a/cxmanage_api/simg.py +++ b/cxmanage_api/simg.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: simg.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -38,7 +41,8 @@ HEADER_LENGTH = 60 MIN_HEADER_LENGTH = 28 -class SIMGHeader: +# pylint: disable=R0913, R0903, R0902 +class SIMGHeader(object): """Container for an SIMG header. >>> from cxmanage_api.simg import SIMGHeader diff --git a/cxmanage_api/tasks.py b/cxmanage_api/tasks.py index 6b5cfde..d241c30 100644 --- a/cxmanage_api/tasks.py +++ b/cxmanage_api/tasks.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: tasks.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -43,7 +46,7 @@ class Task(object): :type args: list """ - def __init__(self, method, *args): + def __init__(self, method, *args, **kwargs): """Default constructor for the Task class.""" self.status = "Queued" self.result = None @@ -51,6 +54,7 @@ class Task(object): self._method = method self._args = args + self._kwargs = kwargs self._finished = Event() def join(self): @@ -70,10 +74,11 @@ class Task(object): """Execute this task. Should only be called by TaskWorker.""" self.status = "In Progress" try: - self.result = self._method(*self._args) + self.result = self._method(*self._args, **self._kwargs) self.status = "Completed" - except Exception as e: - self.error = e + # pylint: disable=W0703 + except Exception as err: + self.error = err self.status = "Failed" self._finished.set() @@ -96,7 +101,7 @@ class TaskQueue(object): self._queue = deque() self._workers = 0 - def put(self, method, *args): + def put(self, method, *args, **kwargs): """Add a task to the task queue, and spawn a worker if we're not full. :param method: Named method to run. @@ -110,7 +115,7 @@ class TaskQueue(object): """ self._lock.acquire() - task = Task(method, *args) + task = Task(method, *args, **kwargs) self._queue.append(task) if self._workers < self.threads: @@ -166,8 +171,11 @@ class TaskWorker(Thread): while True: sleep(self._delay) task = self._task_queue.get() + # pylint: disable=W0212 task._run() - except: + # pylint: disable=W0703 + except Exception: + # pylint: disable=W0212 self._task_queue._remove_worker() DEFAULT_TASK_QUEUE = TaskQueue() diff --git a/cxmanage_api/tftp.py b/cxmanage_api/tftp.py index 02b7c49..e3aaec3 100644 --- a/cxmanage_api/tftp.py +++ b/cxmanage_api/tftp.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: tftp.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -29,22 +32,21 @@ # DAMAGE. -import os -import sys -import atexit import shutil 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 from tftpy.TftpShared import TftpException -class InternalTftp(object): - """Internally serves files using the `Trivial File Transfer Protocol <http://en.wikipedia.org/wiki/Trivial_File_Transfer_Protocol>`_. +class InternalTftp(Thread): + """Internally serves files using the `Trivial File Transfer Protocol \ +<http://en.wikipedia.org/wiki/Trivial_File_Transfer_Protocol>`_. >>> # Typical instantiation ... >>> from cxmanage_api.tftp import InternalTftp @@ -60,56 +62,45 @@ class InternalTftp(object): :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): - """Default constructor for the InternalTftp class.""" + super(InternalTftp, self).__init__() + self.daemon = True + self.tftp_dir = temp_dir() self.verbose = verbose - pipe = os.pipe() - pid = os.fork() - if (not pid): - # Force tftpy to use sys.stdout and sys.stderr - try: - os.dup2(sys.stdout.fileno(), 1) - os.dup2(sys.stderr.fileno(), 2) - - except AttributeError, err_msg: - if (self.verbose): - print ('Passing on exception: %s' % err_msg) - pass - - # Create a PortThread class only if needed ... - class PortThread(Thread): - """Thread that sends the port number through the pipe.""" - def run(self): - """Run function override.""" - # Need to wait for the server to open its socket - while not server.sock: - pass - with os.fdopen(pipe[1], "w") as a_file: - a_file.write("%i\n" % server.sock.getsockname()[1]) - # - # Create an Internal TFTP server thread - # - server = TftpServer(tftproot=self.tftp_dir) - thread = PortThread() - thread.start() - try: - if not self.verbose: - setLogLevel(logging.CRITICAL) - # Start accepting connections ... - server.listen(listenport=port) - except KeyboardInterrupt: - # User @ keyboard cancelled server ... - if (self.verbose): - traceback.format_exc() - sys.exit(0) - - self.server = pid + + self.server = TftpServer(tftproot=self.tftp_dir) self.ip_address = ip_address - with os.fdopen(pipe[0]) as a_fd: - self.port = int(a_fd.readline()) - atexit.register(self.kill) + self.port = port + self.start() + + # 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. """ + if not self.verbose: + setLogLevel(logging.CRITICAL) + self.server.listen(listenport=self.port) def get_address(self, relative_host=None): """Returns the ipv4 address of this server. @@ -136,16 +127,6 @@ class InternalTftp(object): sock.close() return ipv4 - def kill(self): - """Kills the InternalTftpServer. - - >>> i_tftp.kill() - - """ - if (self.server): - os.kill(self.server, 15) - self.server = None - def get_file(self, src, dest): """Download a file from the tftp server to local_path. @@ -153,7 +134,7 @@ class InternalTftp(object): :param src: Source file path on the tftp_server. :type src: string - :param dest: Destination path (on your machine) to copy the TFTP file to. + :param dest: Destination path (local machine) to copy the TFTP file to. :type dest: string """ @@ -222,7 +203,7 @@ class ExternalTftp(object): >>> e_tftp.get_address() '1.2.3.4' - :param relative_host: Unused parameter present only for function signature. + :param relative_host: Unused parameter, for function signature. :type relative_host: None :returns: The ip address of the external TFTP server. diff --git a/cxmanage_api/ubootenv.py b/cxmanage_api/ubootenv.py index b5b8272..14029bb 100644 --- a/cxmanage_api/ubootenv.py +++ b/cxmanage_api/ubootenv.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: ubootenv.py """ + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -33,7 +35,7 @@ import struct from cxmanage_api.simg import has_simg, get_simg_contents from cxmanage_api.crc32 import get_crc32 -from cxmanage_api.cx_exceptions import UnknownBootCmdError +from cxmanage_api.cx_exceptions import UbootenvError ENVIRONMENT_SIZE = 8192 @@ -43,7 +45,7 @@ UBOOTENV_V2_VARIABLES = ["bootcmd0", "init_scsi", "bootcmd_scsi", "init_pxe", "bootcmd_pxe", "devnum"] -class UbootEnv: +class UbootEnv(object): """Represents a U-Boot Environment. >>> from cxmanage_api.ubootenv import UbootEnv @@ -68,6 +70,7 @@ class UbootEnv: part = line.partition("=") self.variables[part[0]] = part[2] + # pylint: disable=R0912 def set_boot_order(self, boot_args): """Sets the boot order specified in the uboot environment. @@ -87,7 +90,7 @@ class UbootEnv: :raises ValueError: If an invalid boot device is specified. :raises ValueError: If 'retry' and 'reset' args are used together. - :raises Exception: If the u-boot environment is unrecognized + :raises UbootenvError: If the u-boot environment is unrecognized """ validate_boot_args(boot_args) @@ -103,7 +106,7 @@ class UbootEnv: elif all(x in self.variables for x in UBOOTENV_V2_VARIABLES): version = 2 else: - raise Exception("Unrecognized u-boot environment") + raise UbootenvError("Unrecognized u-boot environment") for arg in boot_args: if arg == "retry": @@ -117,6 +120,7 @@ class UbootEnv: commands.append("run bootcmd_sata") elif arg.startswith("disk"): try: + # pylint: disable=W0141 dev, part = map(int, arg[4:].split(":")) bootdevice = "%i:%i" % (dev, part) except ValueError: @@ -130,13 +134,15 @@ class UbootEnv: commands.append("run init_scsi && run bootcmd_scsi") elif arg.startswith("disk"): try: + # pylint: disable=W0141 dev, part = map(int, arg[4:].split(":")) bootdevice = "%i:%i" % (dev, part) except ValueError: bootdevice = str(int(arg[4:])) commands.append( - "setenv devnum %s && run init_scsi && run bootcmd_scsi" - % bootdevice) + "setenv devnum %s && run init_scsi && run bootcmd_scsi" + % bootdevice + ) if retry and reset: raise ValueError("retry and reset are mutually exclusive") @@ -159,7 +165,7 @@ class UbootEnv: :returns: Boot order for this U-Boot Environment. :rtype: string - :raises UnknownBootCmdError: If a boot command is unrecognized. + :raises UbootenvError: If a boot command is unrecognized. """ boot_args = [] @@ -171,7 +177,7 @@ class UbootEnv: elif target == "scsi": boot_args.append("disk") else: - raise UnknownBootCmdError("Unrecognized boot target: %s" + raise UbootenvError("Unrecognized boot target: %s" % target) else: if "bootcmd_default" in self.variables: @@ -198,7 +204,7 @@ class UbootEnv: boot_args.append("reset") break else: - raise UnknownBootCmdError("Unrecognized boot command: %s" + raise UbootenvError("Unrecognized boot command: %s" % command) if retry: @@ -208,9 +214,63 @@ class UbootEnv: if not boot_args: boot_args = ["none"] - validate_boot_args(boot_args) # sanity check + validate_boot_args(boot_args) # sanity check return boot_args + + def set_pxe_interface(self, interface): + """Sets the interfacespecified in the uboot environment. + + >>> uboot.set_pxe_interface('eth0') + + .. note:: + * Valid Args: eth0 or eth1 + + :param interface: The interface to set. + :type boot_args: string + + :raises ValueError: If an invalid interface is specified. + + """ + validate_pxe_interface(interface) + if interface == self.get_pxe_interface(): + return + + if interface == "eth0": + self.variables["ethprime"] = "xgmac0" + elif (interface == "eth1"): + self.variables["ethprime"] = "xgmac1" + else: + raise ValueError("Invalid pxe interface: %s" % interface) + + def get_pxe_interface(self): + """Returns a string representation of the pxe interface. + + >>> uboot.get_pxe_interface() + 'eth0' + + :returns: Boot order for this U-Boot Environment. + :rtype: string + :raises ValueError: If the u-boot environment value is not recognized. + + """ + + # This is based on reading the ethprime environment variable, and + # translating from xgmacX to ethX. By default ethprime is not set + # and eth0 is the assumed default (NOTE: this is brittle) + + if "ethprime" in self.variables: + xgmac = self.variables["ethprime"] + if xgmac == "xgmac0": + return "eth0" + elif (xgmac == "xgmac1"): + return "eth1" + else: + raise ValueError("Unrecognized value for ethprime") + else: + return "eth0" + + def get_contents(self): """Returns a raw string representation of the uboot environment. @@ -245,6 +305,7 @@ def validate_boot_args(boot_args): continue elif arg.startswith("disk"): try: + # pylint: disable=W0141 map(int, arg[4:].split(":")) except ValueError: try: @@ -253,3 +314,9 @@ def validate_boot_args(boot_args): raise ValueError("Invalid boot arg: %s" % arg) else: raise ValueError("Invalid boot arg: %s" % arg) + + +def validate_pxe_interface(interface): + """ Validate pxe interface. Raises a ValueError if the args are invalid.""" + if not interface in ["eth0", "eth1"]: + raise ValueError("Invalid pxe interface: %s" % interface) diff --git a/cxmanage_test/__init__.py b/cxmanage_test/__init__.py index 2033b60..d8d5307 100644 --- a/cxmanage_test/__init__.py +++ b/cxmanage_test/__init__.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: __init__.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -29,8 +32,6 @@ # DAMAGE. -""" Various objects used by tests """ - import os import random import tempfile @@ -39,17 +40,20 @@ from cxmanage_api.image import Image def random_file(size): """ Create a random file """ - contents = "".join([chr(random.randint(0, 255)) for a in range(size)]) - fd, filename = tempfile.mkstemp(prefix='cxmanage_test-') - with os.fdopen(fd, "w") as f: - f.write(contents) + contents = "".join([chr(random.randint(0, 255)) for _ in range(size)]) + file_, filename = tempfile.mkstemp(prefix='cxmanage_test-') + with os.fdopen(file_, "w") as file_handle: + file_handle.write(contents) + return filename class TestImage(Image): + """TestImage Class.""" def verify(self): return True -class TestSensor: +# pylint: disable=R0903 +class TestSensor(object): """ Sensor result from bmc/target """ def __init__(self, sensor_name, sensor_reading): self.sensor_name = sensor_name diff --git a/cxmanage_test/fabric_test.py b/cxmanage_test/fabric_test.py index fb234c5..96f76e6 100644 --- a/cxmanage_test/fabric_test.py +++ b/cxmanage_test/fabric_test.py @@ -1,4 +1,6 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: fabric_test.py """ + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -44,12 +46,15 @@ from pyipmi import make_bmc NUM_NODES = 128 ADDRESSES = ["192.168.100.%i" % x for x in range(1, NUM_NODES + 1)] + +# pylint: disable=R0904 class FabricTest(unittest.TestCase): """ Test the various Fabric commands """ def setUp(self): # Set up the controller and add targets self.fabric = Fabric("192.168.100.1", node=DummyNode) - self.nodes = [DummyNode(x) for x in ADDRESSES] + self.nodes = [DummyNode(i) for i in ADDRESSES] + # pylint: disable=W0212 self.fabric._nodes = dict((i, self.nodes[i]) for i in xrange(NUM_NODES)) @@ -75,11 +80,16 @@ class FabricTest(unittest.TestCase): self.assertEqual(node.executed, []) def test_get_uplink_info(self): - """ Test get_mac_addresses command """ + """ Test get_uplink_info command """ self.fabric.get_uplink_info() - self.assertEqual(self.nodes[0].executed, ["get_fabric_uplink_info"]) - for node in self.nodes[1:]: - self.assertEqual(node.executed, []) + for node in self.nodes: + self.assertEqual(node.executed, ["get_uplink_info"]) + + def test_get_uplink_speed(self): + """ Test get_uplink_speed command """ + self.fabric.get_uplink_speed() + for node in self.nodes: + self.assertEqual(node.executed, ["get_uplink_speed"]) def test_get_uplink(self): """ Test get_uplink command """ @@ -140,6 +150,18 @@ class FabricTest(unittest.TestCase): for node in self.nodes: self.assertEqual(node.executed, ["get_boot_order"]) + def test_set_pxe_interface(self): + """ Test set_pxe_interface command """ + self.fabric.set_pxe_interface("eth0") + for node in self.nodes: + self.assertEqual(node.executed, [("set_pxe_interface", "eth0")]) + + def test_get_pxe_interface(self): + """ Test get_pxe_interface command """ + self.fabric.get_pxe_interface() + for node in self.nodes: + self.assertEqual(node.executed, ["get_pxe_interface"]) + def test_get_versions(self): """ Test get_versions command """ self.fabric.get_versions() @@ -157,7 +179,9 @@ class FabricTest(unittest.TestCase): ipmitool_args = "power status" self.fabric.ipmitool_command(ipmitool_args) for node in self.nodes: - self.assertEqual(node.executed, [("ipmitool_command", ipmitool_args)]) + self.assertEqual( + node.executed, [("ipmitool_command", ipmitool_args)] + ) def test_get_server_ip(self): """ Test get_server_ip command """ @@ -169,8 +193,11 @@ class FabricTest(unittest.TestCase): def test_failed_command(self): """ Test a failed command """ - fail_nodes = [DummyFailNode(x) for x in ADDRESSES] - self.fabric._nodes = dict((i, fail_nodes[i]) for i in xrange(NUM_NODES)) + fail_nodes = [DummyFailNode(i) for i in ADDRESSES] + # pylint: disable=W0212 + self.fabric._nodes = dict( + (i, fail_nodes[i]) for i in xrange(NUM_NODES) + ) try: self.fabric.get_power() self.fail() @@ -206,7 +233,7 @@ class FabricTest(unittest.TestCase): # it's there to make sure the ipsrc_mode value gets passed to the bmc. self.assertEqual(bmc.fabric_ipsrc, ipsrc) - def test_apply_factory_default_config(self): + def test_apply_fdc(self): """Test the apply_factory_default_config method""" self.fabric.apply_factory_default_config() @@ -274,26 +301,26 @@ class FabricTest(unittest.TestCase): def test_get_link_stats(self): """Test the get_link_stats() method.""" for i in range(0, 5): - stats = self.fabric.get_link_stats(i) - for nn, node in self.fabric.nodes.items(): + self.fabric.get_link_stats(i) + for node in self.fabric.nodes.values(): self.assertIn(('get_link_stats', i), node.executed) def test_get_linkmap(self): """Test the get_linkmap method""" - maps = self.fabric.get_linkmap() - for nn, node in self.fabric.nodes.items(): + self.fabric.get_linkmap() + for node in self.fabric.nodes.values(): self.assertIn('get_linkmap', node.executed) def test_get_routing_table(self): """Test the get_routing_table method""" - maps = self.fabric.get_routing_table() - for nn, node in self.fabric.nodes.items(): + self.fabric.get_routing_table() + for node in self.fabric.nodes.values(): self.assertIn('get_routing_table', node.executed) - + def test_get_depth_chart(self): """Test the depth_chart method""" - maps = self.fabric.get_depth_chart() - for nn, node in self.fabric.nodes.items(): + self.fabric.get_depth_chart() + for node in self.fabric.nodes.values(): self.assertIn('get_depth_chart', node.executed) def test_get_link_users_factor(self): @@ -347,45 +374,135 @@ class FabricTest(unittest.TestCase): bmc = self.fabric.primary_node.bmc self.assertIn ('fabric_rm_macaddr', bmc.executed) + def test_set_macaddr_base(self): + """Test the set_macaddr_base method""" + self.fabric.set_macaddr_base("00:11:22:33:44:55") + for node in self.fabric.nodes.values(): + if node == self.fabric.primary_node: + self.assertEqual( + node.bmc.executed, + [("fabric_config_set_macaddr_base", "00:11:22:33:44:55")] + ) + else: + self.assertEqual(node.bmc.executed, []) + + def test_get_macaddr_base(self): + """Test the get_macaddr_base method""" + self.assertEqual(self.fabric.get_macaddr_base(), "00:00:00:00:00:00") + for node in self.fabric.nodes.values(): + if node == self.fabric.primary_node: + self.assertEqual( + node.bmc.executed, + ["fabric_config_get_macaddr_base"] + ) + else: + self.assertEqual(node.bmc.executed, []) + + def test_set_macaddr_mask(self): + """Test the set_macaddr_mask method""" + self.fabric.set_macaddr_mask("00:11:22:33:44:55") + for node in self.fabric.nodes.values(): + if node == self.fabric.primary_node: + self.assertEqual( + node.bmc.executed, + [("fabric_config_set_macaddr_mask", "00:11:22:33:44:55")] + ) + else: + self.assertEqual(node.bmc.executed, []) + + def test_get_macaddr_mask(self): + """Test the get_macaddr_mask method""" + self.assertEqual(self.fabric.get_macaddr_mask(), "00:00:00:00:00:00") + for node in self.fabric.nodes.values(): + if node == self.fabric.primary_node: + self.assertEqual( + node.bmc.executed, + ["fabric_config_get_macaddr_mask"] + ) + else: + self.assertEqual(node.bmc.executed, []) + + def test_composite_bmc(self): + """ Test the CompositeBMC member """ + with self.assertRaises(AttributeError): + self.fabric.cbmc.fake_method() + + self.fabric.cbmc.set_chassis_power("off") + results = self.fabric.cbmc.get_chassis_status() + + self.assertEqual(len(results), len(self.fabric.nodes)) + for node_id in self.fabric.nodes: + self.assertFalse(results[node_id].power_on) + + for node in self.fabric.nodes.values(): + self.assertEqual(node.bmc.executed, [ + ("set_chassis_power", "off"), + "get_chassis_status" + ]) + + class DummyNode(object): """ Dummy node for the nodemanager tests """ + + # pylint: disable=W0613 def __init__(self, ip_address, username="admin", password="admin", tftp=None, *args, **kwargs): self.executed = [] + self.power_state = False self.ip_address = ip_address self.tftp = tftp + self.sel = [] self.bmc = make_bmc(DummyBMC, hostname=ip_address, username=username, password=password, verbose=False) + @property + def chassis_id(self): + """Returns 0 for chasis ID.""" + return 0 + + def get_sel(self): + """Simulate get_sel()""" + self.executed.append('get_sel') + return self.sel + def get_power(self): + """Simulate get_power(). """ self.executed.append("get_power") - return False + return self.power_state def set_power(self, mode): + """Simulate set_power(). """ self.executed.append(("set_power", mode)) def get_power_policy(self): + """Simulate get_power_policy(). """ self.executed.append("get_power_policy") return "always-off" def set_power_policy(self, mode): + """Simulate set_power_policy(). """ self.executed.append(("set_power_policy", mode)) def mc_reset(self): + """Simulate mc_reset(). """ self.executed.append("mc_reset") def get_firmware_info(self): + """Simulate get_firmware_info(). """ self.executed.append("get_firmware_info") def is_updatable(self, package, partition_arg="INACTIVE", priority=None): + """Simulate is_updateable(). """ self.executed.append(("is_updatable", package)) def update_firmware(self, package, partition_arg="INACTIVE", priority=None): + """Simulate update_firmware(). """ self.executed.append(("update_firmware", package)) time.sleep(random.randint(0, 2)) def get_sensors(self, name=""): + """Simulate get_sensors(). """ self.executed.append("get_sensors") power_value = "%f (+/- 0) Watts" % random.uniform(0, 10) temp_value = "%f (+/- 0) degrees C" % random.uniform(30, 50) @@ -393,22 +510,37 @@ class DummyNode(object): TestSensor("Node Power", power_value), TestSensor("Board Temp", temp_value) ] - return [x for x in sensors if name.lower() in x.sensor_name.lower()] + return [s for s in sensors if name.lower() in s.sensor_name.lower()] def config_reset(self): + """Simulate config_reset(). """ self.executed.append("config_reset") def set_boot_order(self, boot_args): + """Simulate set_boot_order().""" self.executed.append(("set_boot_order", boot_args)) def get_boot_order(self): + """Simulate get_boot_order(). """ self.executed.append("get_boot_order") return ["disk", "pxe"] + def set_pxe_interface(self, interface): + """Simulate set_pxe_interface(). """ + self.executed.append(("set_pxe_interface", interface)) + + def get_pxe_interface(self): + """Simulate get_pxe_interface(). """ + self.executed.append("get_pxe_interface") + return "eth0" + def get_versions(self): + """Simulate get_versions(). """ self.executed.append("get_versions") - class Result: + # pylint: disable=R0902, R0903 + class Result(object): + """Result Class. """ def __init__(self): self.header = "Calxeda SoC (0x0096CD)" self.hardware_version = "TestBoard X00" @@ -417,13 +549,16 @@ class DummyNode(object): self.ecme_timestamp = "0" self.a9boot_version = "v0.0.0" self.uboot_version = "v0.0.0" + self.chip_name = "Unknown" return Result() def ipmitool_command(self, ipmitool_args): + """Simulate ipmitool_command(). """ self.executed.append(("ipmitool_command", ipmitool_args)) return "Dummy output" def get_ubootenv(self): + """Simulate get_ubootenv(). """ self.executed.append("get_ubootenv") ubootenv = UbootEnv() @@ -431,15 +566,21 @@ class DummyNode(object): ubootenv.variables["bootcmd_default"] = "run bootcmd_sata" return ubootenv - def get_fabric_ipinfo(self): + @staticmethod + def get_fabric_ipinfo(): + """Simulates get_fabric_ipinfo(). """ return {} - def get_server_ip(self, interface, ipv6, user, password, aggressive): + # pylint: disable=R0913 + def get_server_ip(self, interface=None, ipv6=False, user="user1", + password="1Password", aggressive=False): + """Simulate get_server_ip(). """ self.executed.append(("get_server_ip", interface, ipv6, user, password, aggressive)) return "192.168.200.1" def get_fabric_macaddrs(self): + """Simulate get_fabric_macaddrs(). """ self.executed.append("get_fabric_macaddrs") result = {} for node in range(NUM_NODES): @@ -450,13 +591,25 @@ class DummyNode(object): return result def get_fabric_uplink_info(self): + """Simulate get_fabric_uplink_info(). """ self.executed.append('get_fabric_uplink_info') results = {} - for n in range(1, NUM_NODES): - results[n] = {'eth0': 0, 'eth1': 0, 'mgmt': 0} + for nid in range(1, NUM_NODES): + results[nid] = {'eth0': 0, 'eth1': 0, 'mgmt': 0} return results + def get_uplink_info(self): + """Simulate get_uplink_info(). """ + self.executed.append('get_uplink_info') + return 'Node 0: eth0 0, eth1 0, mgmt 0' + + def get_uplink_speed(self): + """Simulate get_uplink_speed(). """ + self.executed.append('get_uplink_speed') + return 1 + def get_link_stats(self, link=0): + """Simulate get_link_stats(). """ self.executed.append(('get_link_stats', link)) return { 'FS_LC%s_BYTE_CNT_0' % link: '0x0', @@ -481,50 +634,71 @@ class DummyNode(object): } def get_linkmap(self): + """Simulate get_linkmap(). """ self.executed.append('get_linkmap') results = {} - for n in range(0, NUM_NODES): - results[n] = {n: {1: 2, 3: 1, 4: 3}} + for nid in range(0, NUM_NODES): + results[nid] = {nid: {1: 2, 3: 1, 4: 3}} return results def get_routing_table(self): + """Simulate get_routing_table(). """ self.executed.append('get_routing_table') results = {} - for n in range(0, NUM_NODES): - results[n] = {n: {1: [0, 0, 0, 3, 0], + for nid in range(0, NUM_NODES): + results[nid] = {nid: {1: [0, 0, 0, 3, 0], 2: [0, 3, 0, 0, 2], 3: [0, 2, 0, 0, 3]}} return results def get_depth_chart(self): + """Simulate get_depth_chart(). """ self.executed.append('get_depth_chart') results = {} - for n in range(0, NUM_NODES): - results[n] = {n: {1: {'shortest': (0, 0)}, + for nid in range(0, NUM_NODES): + results[nid] = {nid: {1: {'shortest': (0, 0)}, 2: {'hops': [(3, 1)], 'shortest': (0, 0)}, 3: {'hops': [(2, 1)], 'shortest': (0, 0)}}} return results def get_uplink(self, iface): + """Simulate get_uplink(). """ self.executed.append(('get_uplink', iface)) return 0 def set_uplink(self, uplink, iface): + """Simulate set_uplink(). """ self.executed.append(('set_uplink', uplink, iface)) + def get_node_fru_version(self): + """Simulate get_node_fru_version(). """ + self.executed.append("get_node_fru_version") + return "0.0" + + def get_slot_fru_version(self): + """Simulate get_slot_fru_version(). """ + self.executed.append("get_slot_fru_version") + return "0.0" + class DummyFailNode(DummyNode): """ Dummy node that should fail on some commands """ class DummyFailError(Exception): + """Dummy Fail Error class.""" pass def get_power(self): + """Simulate get_power(). """ self.executed.append("get_power") raise DummyFailNode.DummyFailError -class DummyImage: +# pylint: disable=R0903 +class DummyImage(object): + """Dummy Image class.""" + def __init__(self, filename, image_type, *args): self.filename = filename self.type = image_type + self.args = args diff --git a/cxmanage_test/image_test.py b/cxmanage_test/image_test.py index 609aa5b..71e8000 100644 --- a/cxmanage_test/image_test.py +++ b/cxmanage_test/image_test.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: image_test.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -39,6 +42,8 @@ from cxmanage_api.tftp import InternalTftp from cxmanage_test import random_file, TestImage + +# pylint: disable=R0904 class ImageTest(unittest.TestCase): """ Tests involving cxmanage images @@ -72,13 +77,14 @@ class ImageTest(unittest.TestCase): self.assertEqual(header.daddr, daddr) self.assertEqual(simg[header.imgoff:], contents) - def test_multiple_uploads(self): + @staticmethod + def test_multiple_uploads(): """ Test to make sure FDs are being closed """ # Create image filename = random_file(1024) image = TestImage(filename, "RAW") - for x in xrange(2048): + for _ in xrange(2048): image.render_to_simg(0, 0) os.remove(filename) diff --git a/cxmanage_test/node_test.py b/cxmanage_test/node_test.py index d5d9445..dbe78a1 100644 --- a/cxmanage_test/node_test.py +++ b/cxmanage_test/node_test.py @@ -1,4 +1,8 @@ -# Copyright (c) 2012, Calxeda Inc. +# pylint: disable=C0302 +"""Unit tests for the Node class.""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -27,7 +31,6 @@ # 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. -"""Unit tests for the Node class.""" import random @@ -49,9 +52,11 @@ from cxmanage_api.cx_exceptions import IPDiscoveryError NUM_NODES = 4 -ADDRESSES = ["192.168.100.%i" % x for x in range(1, NUM_NODES + 1)] +ADDRESSES = ["192.168.100.%i" % n for n in range(1, NUM_NODES + 1)] TFTP = InternalTftp() + +# pylint: disable=R0904, W0201 class NodeTest(unittest.TestCase): """ Tests involving cxmanage Nodes """ @@ -61,6 +66,18 @@ class NodeTest(unittest.TestCase): ipretriever=DummyIPRetriever, verbose=True) for ip in ADDRESSES] + self.server_ip = None + self.fabric_ipsrc = None + self.fabric_ls_policy = None + self.fabric_linkspeed = None + self.fabric_lu_factor = None + + # Give each node a node_id + count = 0 + for node in self.nodes: + node.node_id = count + count = count + 1 + # Set up an internal server self.work_dir = tempfile.mkdtemp(prefix="cxmanage_node_test-") @@ -111,8 +128,12 @@ class NodeTest(unittest.TestCase): self.assertEqual(node.bmc.executed, ["sdr_list"]) self.assertEqual(len(result), 2) - self.assertTrue(result["Node Power"].sensor_reading.endswith("Watts")) - self.assertTrue(result["Board Temp"].sensor_reading.endswith("degrees C")) + self.assertTrue( + result["Node Power"].sensor_reading.endswith("Watts") + ) + self.assertTrue( + result["Board Temp"].sensor_reading.endswith("degrees C") + ) def test_is_updatable(self): """ Test node.is_updatable method """ @@ -270,6 +291,50 @@ class NodeTest(unittest.TestCase): self.assertEqual(result, ["disk", "pxe"]) + def test_set_pxe_interface(self): + """ Test node.set_pxe_interface method """ + for node in self.nodes: + node.set_pxe_interface("eth0") + + partitions = node.bmc.partitions + ubootenv_partition = partitions[5] + unchanged_partitions = [x for x in partitions + if x != ubootenv_partition] + + self.assertEqual(ubootenv_partition.updates, 1) + self.assertEqual(ubootenv_partition.retrieves, 1) + self.assertEqual(ubootenv_partition.checks, 1) + self.assertEqual(ubootenv_partition.activates, 1) + + for partition in unchanged_partitions: + self.assertEqual(partition.updates, 0) + self.assertEqual(partition.retrieves, 0) + self.assertEqual(partition.checks, 0) + self.assertEqual(partition.activates, 0) + + def test_get_pxe_interface(self): + """ Test node.get_pxe_interface method """ + for node in self.nodes: + result = node.get_pxe_interface() + + partitions = node.bmc.partitions + ubootenv_partition = partitions[5] + unchanged_partitions = [x for x in partitions + if x != ubootenv_partition] + + self.assertEqual(ubootenv_partition.updates, 0) + self.assertEqual(ubootenv_partition.retrieves, 1) + self.assertEqual(ubootenv_partition.checks, 0) + self.assertEqual(ubootenv_partition.activates, 0) + + for partition in unchanged_partitions: + self.assertEqual(partition.updates, 0) + self.assertEqual(partition.retrieves, 0) + self.assertEqual(partition.checks, 0) + self.assertEqual(partition.activates, 0) + + self.assertEqual(result, "eth0") + def test_get_versions(self): """ Test node.get_versions method """ for node in self.nodes: @@ -286,8 +351,8 @@ class NodeTest(unittest.TestCase): for node in self.nodes: result = node.get_fabric_ipinfo() - for x in node.bmc.executed: - self.assertEqual(x, "fabric_config_get_ip_info") + for found in node.bmc.executed: + self.assertEqual(found, "fabric_config_get_ip_info") self.assertEqual(result, dict([(i, ADDRESSES[i]) for i in range(NUM_NODES)])) @@ -297,8 +362,8 @@ class NodeTest(unittest.TestCase): for node in self.nodes: result = node.get_fabric_macaddrs() - for x in node.bmc.executed: - self.assertEqual(x, "fabric_config_get_mac_addresses") + for found in node.bmc.executed: + self.assertEqual(found, "fabric_config_get_mac_addresses") self.assertEqual(len(result), NUM_NODES) for node_id in xrange(NUM_NODES): @@ -310,39 +375,56 @@ class NodeTest(unittest.TestCase): def test_get_fabric_uplink_info(self): """ Test node.get_fabric_uplink_info method """ for node in self.nodes: - result = node.get_fabric_uplink_info() + node.get_fabric_uplink_info() + + for found in node.bmc.executed: + self.assertEqual(found, "fabric_config_get_uplink_info") + + def test_get_uplink_info(self): + """ Test node.get_uplink_info method """ + for node in self.nodes: + node.get_uplink_info() + + for found in node.bmc.executed: + self.assertEqual(found, "get_uplink_info") + + def test_get_uplink_speed(self): + """ Test node.get_uplink_info method """ + for node in self.nodes: + node.get_uplink_speed() + + for found in node.bmc.executed: + self.assertEqual(found, "get_uplink_speed") - for x in node.bmc.executed: - self.assertEqual(x, "fabric_config_get_uplink_info") def test_get_linkmap(self): """ Test node.get_linkmap method """ for node in self.nodes: - result = node.get_linkmap() + node.get_linkmap() - for x in node.bmc.executed: - self.assertEqual(x, "fabric_info_get_link_map") + for found in node.bmc.executed: + self.assertEqual(found, "fabric_info_get_link_map") def test_get_routing_table(self): """ Test node.get_routing_table method """ for node in self.nodes: - result = node.get_routing_table() + node.get_routing_table() - for x in node.bmc.executed: - self.assertEqual(x, "fabric_info_get_routing_table") + for found in node.bmc.executed: + self.assertEqual(found, "fabric_info_get_routing_table") def test_get_depth_chart(self): """ Test node.get_depth_chart method """ for node in self.nodes: - result = node.get_depth_chart() + node.get_depth_chart() - for x in node.bmc.executed: - self.assertEqual(x, "fabric_info_get_depth_chart") + for found in node.bmc.executed: + self.assertEqual(found, "fabric_info_get_depth_chart") def test_get_link_stats(self): """ Test node.get_link_stats() """ for node in self.nodes: - result = node.get_link_stats() + node.get_link_stats() self.assertEqual(node.bmc.executed[0], ('fabric_get_linkstats', 0)) def test_get_server_ip(self): @@ -369,9 +451,14 @@ class NodeTest(unittest.TestCase): node.set_uplink(iface=0, uplink=0) self.assertEqual(node.get_uplink(iface=0), 0) - +# pylint: disable=R0902 class DummyBMC(LanBMC): """ Dummy BMC for the node tests """ + + + GUID_UNIQUE = 0 + + def __init__(self, **kwargs): super(DummyBMC, self).__init__(**kwargs) self.executed = [] @@ -385,6 +472,20 @@ class DummyBMC(LanBMC): Partition(6, 11, 1388544, 12288) # ubootenv ] self.ipaddr_base = '192.168.100.1' + self.unique_guid = 'FAKEGUID%s' % DummyBMC.GUID_UNIQUE + DummyBMC.GUID_UNIQUE += 1 + + def guid(self): + """Returns the GUID""" + self.executed.append("guid") + + # pylint: disable=R0903 + class Result(object): + """Results class.""" + def __init__(self, dummybmc): + self.system_guid = dummybmc.unique_guid + self.time_stamp = None + return Result(self) def set_chassis_power(self, mode): """ Set chassis power """ @@ -394,7 +495,9 @@ class DummyBMC(LanBMC): """ Get chassis status """ self.executed.append("get_chassis_status") - class Result: + # pylint: disable=R0903 + class Result(object): + """Results class.""" def __init__(self): self.power_on = False self.power_restore_policy = "always-off" @@ -440,8 +543,11 @@ class DummyBMC(LanBMC): self.partitions[partition].fwinfo.priority = "%8x" % simg.priority self.partitions[partition].fwinfo.daddr = "%8x" % simg.daddr - class Result: + # pylint: disable=R0903 + class Result(object): + """Results class.""" def __init__(self): + """Default constructor for the Result class.""" self.tftp_handle_id = 0 return Result() @@ -459,7 +565,9 @@ class DummyBMC(LanBMC): tftp.put_file("%s/%s" % (work_dir, filename), filename) shutil.rmtree(work_dir) - class Result: + # pylint: disable=R0903 + class Result(object): + """Results class.""" def __init__(self): self.tftp_handle_id = 0 return Result() @@ -477,16 +585,23 @@ class DummyBMC(LanBMC): def get_firmware_status(self, handle): self.executed.append("get_firmware_status") - class Result: + # pylint: disable=R0903 + class Result(object): + """Results class.""" def __init__(self): self.status = "Complete" + + del handle + return Result() def check_firmware(self, partition): self.executed.append(("check_firmware", partition)) self.partitions[partition].checks += 1 - class Result: + # pylint: disable=R0903 + class Result(object): + """Results class.""" def __init__(self): self.crc32 = 0 self.error = None @@ -516,7 +631,9 @@ class DummyBMC(LanBMC): """ Get basic SoC info from this node """ self.executed.append("get_info_basic") - class Result: + # pylint: disable=R0903 + class Result(object): + """Results class.""" def __init__(self): self.iana = int("0x0096CD", 16) self.firmware_version = "ECX-0000-v0.0.0" @@ -527,12 +644,21 @@ class DummyBMC(LanBMC): def get_info_card(self): self.executed.append("info_card") - class Result: + # pylint: disable=R0903 + class Result(object): + """Results class.""" def __init__(self): self.type = "TestBoard" self.revision = "0" return Result() + node_count = 0 + def fabric_get_node_id(self): + self.executed.append('get_node_id') + result = DummyBMC.node_count + DummyBMC.node_count += 1 + return result + def fabric_info_get_link_map(self, filename, tftp_addr=None): """Upload a link_map file from the node to TFTP""" self.executed.append('fabric_info_get_link_map') @@ -544,7 +670,7 @@ class DummyBMC(LanBMC): link_map.append('Link 1: Node 2') link_map.append('Link 3: Node 1') link_map.append('Link 4: Node 3') - + work_dir = tempfile.mkdtemp(prefix="cxmanage_test-") with open('%s/%s' % (work_dir, filename), 'w') as lm_file: for lmap in link_map: @@ -574,7 +700,7 @@ class DummyBMC(LanBMC): routing_table.append('Node 13: rt - 0.2.0.0.1') routing_table.append('Node 14: rt - 0.2.0.0.1') routing_table.append('Node 15: rt - 0.2.0.0.1') - + work_dir = tempfile.mkdtemp(prefix="cxmanage_test-") with open('%s/%s' % (work_dir, filename), 'w') as rt_file: for rtable in routing_table: @@ -597,23 +723,68 @@ class DummyBMC(LanBMC): raise IpmiError('No tftp address!') depth_chart = [] - depth_chart.append('Node 1: Shortest Distance 0 hops via neighbor 0: other hops/neighbors -') - depth_chart.append('Node 2: Shortest Distance 0 hops via neighbor 0: other hops/neighbors - 1/3') - depth_chart.append('Node 3: Shortest Distance 0 hops via neighbor 0: other hops/neighbors - 1/2') - depth_chart.append('Node 4: Shortest Distance 2 hops via neighbor 6: other hops/neighbors - 3/7') - depth_chart.append('Node 5: Shortest Distance 3 hops via neighbor 4: other hops/neighbors -') - depth_chart.append('Node 6: Shortest Distance 1 hops via neighbor 2: other hops/neighbors -') - depth_chart.append('Node 7: Shortest Distance 2 hops via neighbor 6: other hops/neighbors - 3/4') - depth_chart.append('Node 8: Shortest Distance 3 hops via neighbor 10: other hops/neighbors - 4/11') - depth_chart.append('Node 9: Shortest Distance 4 hops via neighbor 8: other hops/neighbors -') - depth_chart.append('Node 10: Shortest Distance 2 hops via neighbor 6: other hops/neighbors -') - depth_chart.append('Node 11: Shortest Distance 3 hops via neighbor 10: other hops/neighbors - 4/8') - depth_chart.append('Node 12: Shortest Distance 4 hops via neighbor 14: other hops/neighbors - 5/15') - depth_chart.append('Node 13: Shortest Distance 5 hops via neighbor 12: other hops/neighbors -') - depth_chart.append('Node 14: Shortest Distance 3 hops via neighbor 10: other hops/neighbors -') - depth_chart.append('Node 15: Shortest Distance 4 hops via neighbor 14: other hops/neighbors - 5/12') - - + depth_chart.append( + 'Node 1: Shortest Distance 0 hops via neighbor 0: ' + + 'other hops/neighbors -' + ) + depth_chart.append( + 'Node 2: Shortest Distance 0 hops via neighbor 0: ' + + 'other hops/neighbors - 1/3' + ) + depth_chart.append( + 'Node 3: Shortest Distance 0 hops via neighbor 0: ' + + 'other hops/neighbors - 1/2' + ) + depth_chart.append( + 'Node 4: Shortest Distance 2 hops via neighbor 6: ' + + 'other hops/neighbors - 3/7' + ) + depth_chart.append( + 'Node 5: Shortest Distance 3 hops via neighbor 4: ' + + 'other hops/neighbors -' + ) + depth_chart.append( + 'Node 6: Shortest Distance 1 hops via neighbor 2: ' + + 'other hops/neighbors -' + ) + depth_chart.append( + 'Node 7: Shortest Distance 2 hops via neighbor 6: ' + + 'other hops/neighbors - 3/4' + ) + depth_chart.append( + 'Node 8: Shortest Distance 3 hops via neighbor 10: ' + + 'other hops/neighbors - 4/11' + ) + depth_chart.append( + 'Node 9: Shortest Distance 4 hops via neighbor 8: ' + + 'other hops/neighbors -' + ) + depth_chart.append( + 'Node 10: Shortest Distance 2 hops via neighbor 6: ' + + 'other hops/neighbors -' + ) + depth_chart.append( + 'Node 11: Shortest Distance 3 hops via neighbor 10: ' + + 'other hops/neighbors - 4/8' + ) + depth_chart.append( + 'Node 12: Shortest Distance 4 hops via neighbor 14: ' + + 'other hops/neighbors - 5/15' + ) + depth_chart.append( + 'Node 13: Shortest Distance 5 hops via neighbor 12: ' + + 'other hops/neighbors -' + ) + depth_chart.append( + 'Node 14: Shortest Distance 3 hops via neighbor 10: ' + + 'other hops/neighbors -' + ) + depth_chart.append( + 'Node 15: Shortest Distance 4 hops via neighbor 14: ' + + 'other hops/neighbors - 5/12' + ) + + work_dir = tempfile.mkdtemp(prefix="cxmanage_test-") with open('%s/%s' % (work_dir, filename), 'w') as dc_file: for dchart in depth_chart: @@ -628,6 +799,7 @@ class DummyBMC(LanBMC): shutil.rmtree(work_dir) + # pylint: disable=W0222 def fabric_get_linkstats(self, filename, tftp_addr=None, link=None): """Upload a link_stats file from the node to TFTP""" @@ -797,28 +969,69 @@ class DummyBMC(LanBMC): self.fabric_lu_factor = lu_factor self.executed.append('fabric_config_set_link_users_factor') + def fabric_config_set_macaddr_base(self, macaddr): + self.executed.append(('fabric_config_set_macaddr_base', macaddr)) + + def fabric_config_get_macaddr_base(self): + self.executed.append('fabric_config_get_macaddr_base') + return "00:00:00:00:00:00" + + def fabric_config_set_macaddr_mask(self, mask): + self.executed.append(('fabric_config_set_macaddr_mask', mask)) + + def fabric_config_get_macaddr_mask(self): + self.executed.append('fabric_config_get_macaddr_mask') + return "00:00:00:00:00:00" + def fabric_add_macaddr(self, nodeid=0, iface=0, macaddr=None): self.executed.append('fabric_add_macaddr') def fabric_rm_macaddr(self, nodeid=0, iface=0, macaddr=None): self.executed.append('fabric_rm_macaddr') + def fabric_get_uplink_info(self): + """Corresponds to Node.get_uplink_info()""" + self.executed.append('get_uplink_info') + return 'Node 0: eth0 0, eth1 0, mgmt 0' + + def fabric_get_uplink_speed(self): + """Corresponds to Node.get_uplink_speed()""" + self.executed.append('get_uplink_speed') + return 1 + + def fru_read(self, fru_number, filename): + if fru_number == 81: + self.executed.append('node_fru_read') + elif fru_number == 82: + self.executed.append('slot_fru_read') + else: + self.executed.append('fru_read') -class Partition: - def __init__(self, partition, type, offset=0, + with open(filename, "w") as fru_image: + # Writes a fake FRU image with version "0.0" + fru_image.write("x00" * 516 + "0.0" + "x00"*7673) + + def pmic_get_version(self): + return "0" + + +# pylint: disable=R0913, R0903 +class Partition(object): + """Partition class.""" + def __init__(self, partition, type_, offset=0, size=0, priority=0, daddr=0, in_use=None): self.updates = 0 self.retrieves = 0 self.checks = 0 self.activates = 0 - self.fwinfo = FWInfoEntry(partition, type, offset, size, priority, + self.fwinfo = FWInfoEntry(partition, type_, offset, size, priority, daddr, in_use) -class FWInfoEntry: +class FWInfoEntry(object): """ Firmware info for a single partition """ - def __init__(self, partition, type, offset=0, size=0, priority=0, daddr=0, + def __init__(self, partition, type_, offset=0, size=0, priority=0, daddr=0, in_use=None): self.partition = "%2i" % partition self.type = { @@ -826,7 +1039,7 @@ class FWInfoEntry: 3: "03 (SOC_ELF)", 10: "0a (CDB)", 11: "0b (UBOOTENV)" - }[type] + }[type_] self.offset = "%8x" % offset self.size = "%8x" % size self.priority = "%8x" % priority @@ -847,7 +1060,7 @@ class DummyUbootEnv(UbootEnv): """ Do nothing """ pass - +# pylint: disable=R0903 class DummyIPRetriever(object): """ Dummy IP retriever """ diff --git a/cxmanage_test/tasks_test.py b/cxmanage_test/tasks_test.py index a936b06..2d7b9a3 100644 --- a/cxmanage_test/tasks_test.py +++ b/cxmanage_test/tasks_test.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: task_test.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -28,16 +31,21 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. + import unittest import time from cxmanage_api.tasks import TaskQueue + +# pylint: disable=R0904 class TaskTest(unittest.TestCase): + """Test for the TaskQueue Class.""" + def test_task_queue(self): """ Test the task queue """ task_queue = TaskQueue() - counters = [Counter() for x in xrange(128)] + counters = [Counter() for _ in xrange(128)] tasks = [task_queue.put(counters[i].add, i) for i in xrange(128)] for task in tasks: @@ -61,6 +69,8 @@ class TaskTest(unittest.TestCase): self.assertGreaterEqual(finish - start, 2.0) + +# pylint: disable=R0903 class Counter(object): """ Simple counter object for testing purposes """ def __init__(self): diff --git a/cxmanage_test/tftp_test.py b/cxmanage_test/tftp_test.py index 784211a..afd258f 100644 --- a/cxmanage_test/tftp_test.py +++ b/cxmanage_test/tftp_test.py @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: tftp_test.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -27,7 +30,11 @@ # 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. -"""Unit tests for the Internal and External Tftp classes.""" + + +# +# Unit tests for the Internal and External Tftp classes. +# import os @@ -51,6 +58,7 @@ def _get_relative_host(): except socket.error: raise +# pylint: disable=R0904 class InternalTftpTest(unittest.TestCase): """ Tests the functions of the InternalTftp class.""" @@ -79,7 +87,7 @@ class InternalTftpTest(unittest.TestCase): self.assertEqual(open(filename).read(), contents) os.remove(filename) - def test_get_address_with_relative_host(self): + def test_get_address_with_relhost(self): """Tests the get_address(relative_host) function with a relative_host specified. """ @@ -97,6 +105,7 @@ class InternalTftpTest(unittest.TestCase): sock.close() +# pylint: disable=R0904 class ExternalTftpTest(unittest.TestCase): """Tests the ExternalTftp class. ..note: diff --git a/pylint.rc b/pylint.rc new file mode 100644 index 0000000..124425b --- /dev/null +++ b/pylint.rc @@ -0,0 +1,274 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# I0011 - locally disabling PyLint +# R0801 - Similar lines in 2 files (mostly affects unit tests) +disable=I0011, R0801 + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + +# Template used to display messages. This is a python new-style format string +# used to format the massage information. See doc for all details +#msg-template= + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct attribute names in class +# bodies +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )?<?https?://\S+>?$ + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright (c) 2012, Calxeda Inc. +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -32,6 +32,7 @@ import unittest +import xmlrunner from cxmanage_test import tftp_test, image_test, node_test, fabric_test, \ tasks_test @@ -43,7 +44,7 @@ def main(): suite = unittest.TestSuite() for module in test_modules: suite.addTest(loader.loadTestsFromModule(module)) - unittest.TextTestRunner(verbosity=2).run(suite) + xmlrunner.XMLTestRunner(verbosity=2).run(suite) if __name__ == "__main__": main() diff --git a/scripts/cxmanage b/scripts/cxmanage index 101b30b..66c6269 100755 --- a/scripts/cxmanage +++ b/scripts/cxmanage @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright (c) 2012, Calxeda Inc. +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -36,20 +36,25 @@ import pkg_resources import subprocess import sys -from cxmanage.commands.power import power_command, power_status_command, \ - power_policy_command, power_policy_status_command -from cxmanage.commands.mc import mcreset_command -from cxmanage.commands.fw import fwupdate_command, fwinfo_command -from cxmanage.commands.sensor import sensor_command -from cxmanage.commands.fabric import ipinfo_command, macaddrs_command -from cxmanage.commands.config import config_reset_command, config_boot_command -from cxmanage.commands.info import info_command -from cxmanage.commands.ipmitool import ipmitool_command -from cxmanage.commands.ipdiscover import ipdiscover_command +import pyipmi +import cxmanage_api +from cxmanage_api.cli.commands.power import power_command, \ + power_status_command, power_policy_command, power_policy_status_command +from cxmanage_api.cli.commands.mc import mcreset_command +from cxmanage_api.cli.commands.fw import fwupdate_command, fwinfo_command +from cxmanage_api.cli.commands.sensor import sensor_command +from cxmanage_api.cli.commands.fabric import ipinfo_command, macaddrs_command +from cxmanage_api.cli.commands.config import config_reset_command, \ + config_boot_command, config_pxe_command +from cxmanage_api.cli.commands.info import info_command +from cxmanage_api.cli.commands.ipmitool import ipmitool_command +from cxmanage_api.cli.commands.ipdiscover import ipdiscover_command +from cxmanage_api.cli.commands.tspackage import tspackage_command +from cxmanage_api.cli.commands.eeprom import eepromupdate_command -PYIPMI_VERSION = '0.7.1' -IPMITOOL_VERSION = '1.8.11.0-cx5' +PYIPMI_VERSION = '0.9.1' +IPMITOOL_VERSION = '1.8.11.0-cx8' PARSER_EPILOG = """examples: @@ -82,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(): @@ -91,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', @@ -130,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() @@ -168,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', @@ -195,6 +204,7 @@ def build_parser(): 'NEWEST', 'INACTIVE' ])) + simg_args = fwupdate.add_mutually_exclusive_group() simg_args.add_argument('--force-simg', help='Force addition of SIMG header', @@ -214,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() @@ -248,14 +273,19 @@ def build_parser(): type=lambda x: [] if x == 'none' else x.split(',')) boot.set_defaults(func=config_boot_command) - #info command + pxe = config_subs.add_parser('pxe', + help='set pxe interface') + pxe.add_argument('interface', help='pxe interface to use') + pxe.set_defaults(func=config_pxe_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', @@ -265,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', @@ -284,6 +314,11 @@ def build_parser(): parser.add_argument('hostname', help='nodes to operate on (see examples below)') + # tspackage command ("troubleshoot package") + tspackage = subparsers.add_parser('tspackage', + help='Save information about this node/fabric to a .tar') + tspackage.set_defaults(func=tspackage_command) + return parser @@ -302,29 +337,15 @@ def validate_args(args): sys.exit('Invalid argument --version when supplied with --skip-simg') -def print_version(): - """ Print the current version of cxmanage """ - version = pkg_resources.require('cxmanage')[0].version - print "cxmanage version %s" % version - - def check_versions(): """Check versions of dependencies""" # Check pyipmi version - try: - pkg_resources.require('pyipmi>=%s' % PYIPMI_VERSION) - except pkg_resources.DistributionNotFound: - print 'ERROR: cxmanage requires pyipmi version %s'\ - % PYIPMI_VERSION - print 'No existing version was found.' - sys.exit(1) - except pkg_resources.VersionConflict: - version = pkg_resources.require('pyipmi')[0].version + if (pkg_resources.parse_version(pyipmi.__version__) < + pkg_resources.parse_version(PYIPMI_VERSION)): print 'ERROR: cxmanage requires pyipmi version %s' % PYIPMI_VERSION - print 'Current pyipmi version is %s' % version + print 'Current pyipmi version is %s' % pyipmi.__version__ sys.exit(1) - # Check ipmitool version if 'IPMITOOL_PATH' in os.environ: args = [os.environ['IPMITOOL_PATH'], '-V'] @@ -351,7 +372,7 @@ def main(): """Get args and go""" for arg in sys.argv[1:]: if arg in ['-V', '--version']: - print_version() + print "cxmanage version %s" % cxmanage_api.__version__ sys.exit(0) elif arg[0] != '-': break diff --git a/scripts/cxmux b/scripts/cxmux new file mode 100755 index 0000000..0630efd --- /dev/null +++ b/scripts/cxmux @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +# Copyright (c) 2012-2013, 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. + + +import os +import sys + +from optparse import OptionParser + +import cxmanage_api.fabric + + +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.add_option( + "--virt-env", + action="store", type="string", dest="virt_env", + help="Calls workon <virtual_environment> before spawning a window" + ) + 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]) + + if options.virt_env: + command = 'workon %s; ' % options.virt_env + command + + 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 "%s %s"' % (command, ip)) + os.system('tmux send-keys Enter') + continue + + os.system('tmux split-window -h') + os.system('tmux send-keys "%s %s"' % (command, ip)) + os.system('tmux send-keys Enter') + 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 + ) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/sol_tabs b/scripts/sol_tabs index c5cb9fe..d6626ca 100755 --- a/scripts/sol_tabs +++ b/scripts/sol_tabs @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright (c) 2012, Calxeda Inc. +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -1,4 +1,7 @@ -# Copyright (c) 2012, Calxeda Inc. +"""Calxeda: setup.py""" + + +# Copyright (c) 2012-2013, Calxeda Inc. # # All rights reserved. # @@ -31,19 +34,33 @@ from setuptools import setup +def get_version(): + """ Parse __init__.py to find the package version """ + for line in open("cxmanage_api/__init__.py"): + key, delim, value = line.partition("=") + if key.strip() == "__version__" and delim == "=": + return value.strip().strip("'\"") + raise Exception("Failed to parse cxmanage package version from __init__.py") + setup( name='cxmanage', - version='0.8.2', - packages=['cxmanage', 'cxmanage.commands', 'cxmanage_api'], - scripts=['scripts/cxmanage', 'scripts/sol_tabs'], + version=get_version(), + packages=[ + 'cxmanage_api', + 'cxmanage_api.cli', + 'cxmanage_api.cli.commands', + 'cxmanage_test' + ], + scripts=['scripts/cxmanage', 'scripts/sol_tabs', 'scripts/cxmux'], description='Calxeda Management Utility', # NOTE: As of right now, the pyipmi version requirement needs to be updated # at the top of scripts/cxmanage as well. install_requires=[ 'tftpy', 'pexpect', - 'pyipmi>=0.7.1', + 'pyipmi>=0.9.1', 'argparse', + 'unittest-xml-reporting<1.6.0' ], extras_require={ 'docs': ['sphinx', 'cloud_sptheme'], |