summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Silverstone <daniel.silverstone@codethink.co.uk>2013-11-19 17:06:33 +0000
committerDaniel Silverstone <daniel.silverstone@codethink.co.uk>2013-11-19 17:06:33 +0000
commit54116d3c486ed7aa49f284e4cad9e6e7c293bea6 (patch)
treefa32a75ebba262def85af97cc34a7db69bc69034
parent7087e92d40310d9bb6c8b4a6bb1baf7c3b73bee7 (diff)
parent4da2f9c7eaa95ebf357eeca3a497b6a206675ef8 (diff)
downloadcxmanage-baserock/morph.tar.gz
Merge tag 'v0.10.2' into baserock/morphbaserock/morph
v0.10 post-release tag for pyinstaller support
-rw-r--r--.gitignore2
-rw-r--r--LICENSE2
-rw-r--r--cxmanage_api/__init__.py12
-rw-r--r--cxmanage_api/cli/__init__.py (renamed from cxmanage/__init__.py)85
-rw-r--r--cxmanage_api/cli/commands/__init__.py (renamed from cxmanage/commands/__init__.py)5
-rw-r--r--cxmanage_api/cli/commands/config.py (renamed from cxmanage/commands/config.py)62
-rw-r--r--cxmanage_api/cli/commands/eeprom.py130
-rw-r--r--cxmanage_api/cli/commands/fabric.py (renamed from cxmanage/commands/fabric.py)9
-rw-r--r--cxmanage_api/cli/commands/fw.py (renamed from cxmanage/commands/fw.py)32
-rw-r--r--cxmanage_api/cli/commands/info.py (renamed from cxmanage/commands/info.py)30
-rw-r--r--cxmanage_api/cli/commands/ipdiscover.py (renamed from cxmanage/commands/ipdiscover.py)7
-rw-r--r--cxmanage_api/cli/commands/ipmitool.py (renamed from cxmanage/commands/ipmitool.py)7
-rw-r--r--cxmanage_api/cli/commands/mc.py (renamed from cxmanage/commands/mc.py)10
-rw-r--r--cxmanage_api/cli/commands/power.py (renamed from cxmanage/commands/power.py)14
-rw-r--r--cxmanage_api/cli/commands/sensor.py (renamed from cxmanage/commands/sensor.py)8
-rw-r--r--cxmanage_api/cli/commands/tspackage.py464
-rw-r--r--cxmanage_api/crc32.py2
-rw-r--r--cxmanage_api/cx_exceptions.py90
-rwxr-xr-xcxmanage_api/docs/generate_api_rst.py4
-rw-r--r--cxmanage_api/docs/source/conf.py1
-rw-r--r--cxmanage_api/docs/source/index.rst1
-rw-r--r--cxmanage_api/fabric.py315
-rw-r--r--cxmanage_api/firmware_package.py28
-rw-r--r--cxmanage_api/image.py14
-rw-r--r--cxmanage_api/ip_retriever.py48
-rw-r--r--cxmanage_api/loggers.py398
-rw-r--r--cxmanage_api/node.py881
-rw-r--r--cxmanage_api/simg.py8
-rw-r--r--cxmanage_api/tasks.py24
-rw-r--r--cxmanage_api/tftp.py107
-rw-r--r--cxmanage_api/ubootenv.py89
-rw-r--r--cxmanage_test/__init__.py20
-rw-r--r--cxmanage_test/fabric_test.py240
-rw-r--r--cxmanage_test/image_test.py12
-rw-r--r--cxmanage_test/node_test.py325
-rw-r--r--cxmanage_test/tasks_test.py14
-rw-r--r--cxmanage_test/tftp_test.py15
-rw-r--r--pylint.rc274
-rwxr-xr-xrun_tests5
-rwxr-xr-xscripts/cxmanage111
-rwxr-xr-xscripts/cxmux102
-rwxr-xr-xscripts/sol_tabs2
-rw-r--r--setup.py27
43 files changed, 3324 insertions, 712 deletions
diff --git a/.gitignore b/.gitignore
index 6e62db1..7a69fa9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
tags
*.pyc
cxmanage.egg-info
+.project
+.pydevproject
diff --git a/LICENSE b/LICENSE
index affd7ab..89da6d8 100644
--- a/LICENSE
+++ b/LICENSE
@@ -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
diff --git a/run_tests b/run_tests
index f0d7831..dc9a7b3 100755
--- a/run_tests
+++ b/run_tests
@@ -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.
#
diff --git a/setup.py b/setup.py
index bd49b13..22e5533 100644
--- a/setup.py
+++ b/setup.py
@@ -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'],