summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorge Kraft <george.kraft@calxeda.com>2013-08-08 12:14:22 -0500
committerGeorge Kraft <george.kraft@calxeda.com>2013-08-08 12:14:22 -0500
commitbeb27f950677e5a21477446257dd33a2e9189840 (patch)
tree74a662a826f3fcfbe3bbe485022198d7a804cf6e
parentf77f6ec528270ba36869c789a7f58b81cb368a67 (diff)
parentf09a00207940c1413a7e3f5fe728479cf8f1f079 (diff)
downloadcxmanage-0.9.0-win32_support.tar.gz
Merge tag 'v0.9.0' into win32_supportv0.9.0-win32_support
July 2013 release candidate 1 Conflicts: cxmanage_api/node.py
-rw-r--r--cxmanage/__init__.py13
-rw-r--r--cxmanage/commands/config.py48
-rw-r--r--cxmanage/commands/fw.py3
-rw-r--r--cxmanage/commands/info.py12
-rw-r--r--cxmanage/commands/tspackage.py369
-rw-r--r--cxmanage_api/cx_exceptions.py44
-rw-r--r--cxmanage_api/docs/source/index.rst1
-rw-r--r--cxmanage_api/fabric.py225
-rw-r--r--cxmanage_api/image.py2
-rw-r--r--cxmanage_api/loggers.py397
-rw-r--r--cxmanage_api/node.py465
-rw-r--r--cxmanage_api/tasks.py9
-rw-r--r--cxmanage_api/ubootenv.py76
-rw-r--r--cxmanage_test/fabric_test.py86
-rw-r--r--cxmanage_test/node_test.py64
-rwxr-xr-xscripts/cxmanage18
-rw-r--r--setup.py4
17 files changed, 1585 insertions, 251 deletions
diff --git a/cxmanage/__init__.py b/cxmanage/__init__.py
index 50b760a..e2d416a 100644
--- a/cxmanage/__init__.py
+++ b/cxmanage/__init__.py
@@ -322,3 +322,16 @@ def _print_command_status(tasks, counter):
dots = "".join(["." for x in range(counter % 4)]).ljust(3)
sys.stdout.write(message % (successes, errors, nodes_left, dots))
sys.stdout.flush()
+
+
+# These are needed for ipinfo and whenever version information is printed
+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")
+]
diff --git a/cxmanage/commands/config.py b/cxmanage/commands/config.py
index ca80928..3d5b060 100644
--- a/cxmanage/commands/config.py
+++ b/cxmanage/commands/config.py
@@ -30,7 +30,8 @@
from cxmanage import get_tftp, get_nodes, get_node_strings, run_command
-from cxmanage_api.ubootenv import UbootEnv, validate_boot_args
+from cxmanage_api.ubootenv import UbootEnv, validate_boot_args, \
+ validate_pxe_interface
def config_reset_command(args):
@@ -92,3 +93,48 @@ 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..."
+
+ results, 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):
+ 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/commands/fw.py b/cxmanage/commands/fw.py
index 87f810b..99ed4fe 100644
--- a/cxmanage/commands/fw.py
+++ b/cxmanage/commands/fw.py
@@ -28,6 +28,7 @@
# 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, \
@@ -55,7 +56,7 @@ def fwupdate_command(args):
print "Updating firmware..."
results, errors = run_command(args, nodes, "update_firmware", package,
- args.partition, args.priority)
+ args.partition, args.priority)
if errors:
print "ERROR: Firmware update failed."
return True
diff --git a/cxmanage/commands/info.py b/cxmanage/commands/info.py
index d002906..b1a03c0 100644
--- a/cxmanage/commands/info.py
+++ b/cxmanage/commands/info.py
@@ -29,6 +29,7 @@
# DAMAGE.
from cxmanage import get_tftp, get_nodes, get_node_strings, run_command
+from cxmanage import COMPONENTS
def info_command(args):
@@ -41,16 +42,7 @@ 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")
- ]
+ components = COMPONENTS
tftp = get_tftp(args)
nodes = get_nodes(args, tftp)
diff --git a/cxmanage/commands/tspackage.py b/cxmanage/commands/tspackage.py
new file mode 100644
index 0000000..1a37e6e
--- /dev/null
+++ b/cxmanage/commands/tspackage.py
@@ -0,0 +1,369 @@
+#!/usr/bin/env python
+
+# Copyright 2013 Calxeda, Inc. All Rights Reserved.
+
+
+import os
+import time
+import shutil
+import tarfile
+import tempfile
+
+from cxmanage import get_tftp, get_nodes, run_command
+from cxmanage import 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
+
+ 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)
+
+ # 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_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")
+
+ # This will be used when writing version info to file
+ components = COMPONENTS
+
+ for node in nodes:
+ lines = [] # The lines of text to write to file
+
+ # Since this is the first line of the file, we don't need a \n
+ write_to_file(
+ node,
+ "[ Version Info for Node %d ]" % node.node_id,
+ add_newlines=False
+ )
+
+ lines.append("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
+ )
+ for var, description in components:
+ if hasattr(info_result, var):
+ version = getattr(info_result, var)
+ lines.append("%s: %s" % (description.ljust(19), 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)
+
+
+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)
+
+ 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_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:
+ # join() doesn't add a newline before the first item
+ to_write[0] = "\n" + to_write[0]
+ node_file.write("\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/cx_exceptions.py b/cxmanage_api/cx_exceptions.py
index 410b5d7..f390392 100644
--- a/cxmanage_api/cx_exceptions.py
+++ b/cxmanage_api/cx_exceptions.py
@@ -109,31 +109,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 +259,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):
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..bd8d6a7 100644
--- a/cxmanage_api/fabric.py
+++ b/cxmanage_api/fabric.py
@@ -56,6 +56,53 @@ 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
+
def __init__(self, ip_address, username="admin", password="admin",
tftp=None, ecme_tftp_port=5001, task_queue=None,
verbose=False, node=None):
@@ -68,6 +115,7 @@ class Fabric(object):
self.task_queue = task_queue
self.verbose = verbose
self.node = node
+ self.cbmc = Fabric.CompositeBMC(self)
self._nodes = {}
@@ -77,9 +125,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 +149,9 @@ class Fabric(object):
:rtype: `Tftp <tftp.html>`_
"""
+ if (not self._tftp):
+ self._tftp = InternalTftp()
+
return self._tftp
@tftp.setter
@@ -137,7 +185,8 @@ class Fabric(object):
"""
if not self._nodes:
- self._discover_nodes(self.ip_address)
+ self.refresh()
+
return self._nodes
@property
@@ -154,6 +203,23 @@ class Fabric(object):
"""
return self.nodes[0]
+ def refresh(self):
+ """Gets the nodes of this fabric by pulling IP info from a BMC."""
+ self._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():
+ self._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)
+ self._nodes[node_id].node_id = node_id
+
def get_mac_addresses(self):
"""Gets MAC addresses from all nodes.
@@ -165,10 +231,6 @@ 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
@@ -476,6 +538,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.
@@ -725,6 +822,50 @@ 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 +938,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 +952,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 presented for each
+ >>> # node in the fabric.
+ >>> #
+
:param link: The link to get stats for (0-4).
:type link: integer
@@ -827,6 +995,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 +1011,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 +1030,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
@@ -884,21 +1075,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/image.py b/cxmanage_api/image.py
index 67802e4..7cbd59f 100644
--- a/cxmanage_api/image.py
+++ b/cxmanage_api/image.py
@@ -152,7 +152,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/loggers.py b/cxmanage_api/loggers.py
new file mode 100644
index 0000000..da7c202
--- /dev/null
+++ b/cxmanage_api/loggers.py
@@ -0,0 +1,397 @@
+# Copyright (c) 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)
+
+ 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 efe9a0a..5408555 100644
--- a/cxmanage_api/node.py
+++ b/cxmanage_api/node.py
@@ -31,23 +31,26 @@
import os
import re
-import subprocess
import time
+import shutil
+import tempfile
+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
class Node(object):
@@ -217,10 +220,7 @@ 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):
"""Send an IPMI power command to this target.
@@ -238,10 +238,7 @@ class Node(object):
:type mode: string
"""
- try:
- self.bmc.set_chassis_power(mode=mode)
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ self.bmc.set_chassis_power(mode=mode)
def get_power_policy(self):
"""Return power status reported by IPMI.
@@ -255,10 +252,7 @@ class Node(object):
: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 +267,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 +281,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 +300,26 @@ 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_list()
+
def get_sensors(self, search=""):
"""Get a list of sensor objects that match search criteria.
@@ -357,11 +363,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 +518,18 @@ 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.
"""
- 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,7 +568,6 @@ 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.
"""
@@ -627,31 +613,119 @@ 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, "rb").read())
+
+ logger.info(
+ "Done getting old ubootenv image"
+ )
+
try:
ubootenv = self.ubootenv(open(image.filename, "rb").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, "wb") as f:
@@ -661,11 +735,20 @@ class Node(object):
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 +764,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,22 +782,39 @@ 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 config_reset(self):
"""Resets configuration to factory defaults.
@@ -714,25 +822,23 @@ class Node(object):
>>> node.config_reset()
:raises IpmiError: If errors in the command occur with BMC communication.
- :raises Exception: If there are errors within the command response.
"""
- try:
- # Reset CDB
- result = self.bmc.reset_firmware()
- if (hasattr(result, "error")):
- raise Exception(result.error)
+ # Reset CDB
+ result = 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.
@@ -770,6 +876,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 f:
+ f.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.
@@ -788,10 +929,7 @@ class Node(object):
: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"),
@@ -909,12 +1047,9 @@ class Node(object):
:raises TftpException: If the TFTP transfer fails.
"""
- try:
- filename = self._run_fabric_command(
- function_name='fabric_config_get_ip_info',
- )
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ filename = self._run_fabric_command(
+ function_name='fabric_config_get_ip_info'
+ )
# Parse addresses from ipinfo file
results = {}
@@ -938,6 +1073,20 @@ class Node(object):
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
@@ -945,13 +1094,9 @@ class Node(object):
:raises TftpException: If the TFTP transfer fails.
"""
- try:
- filename = self._run_fabric_command(
- function_name='fabric_config_get_mac_addresses'
- )
-
- except IpmiError as e:
- raise IpmiError(self._parse_ipmierror(e))
+ filename = self._run_fabric_command(
+ function_name='fabric_config_get_mac_addresses'
+ )
# Parse addresses from ipinfo file
results = {}
@@ -1015,6 +1160,27 @@ class Node(object):
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
@@ -1052,6 +1218,9 @@ class Node(object):
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 +1228,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):
@@ -1083,6 +1249,9 @@ class Node(object):
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 +1259,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):
@@ -1117,6 +1283,11 @@ class Node(object):
"""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 +1296,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):
@@ -1197,7 +1365,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 +1373,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 +1395,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 +1404,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 +1414,11 @@ 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 _run_fabric_command(self, function_name, **kwargs):
"""Handles the basics of sending a node a command for fabric data."""
@@ -1269,15 +1429,11 @@ class Node(object):
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))
+ getattr(self.bmc, function_name)(
+ filename=basename,
+ tftp_addr=self.tftp_address,
+ **kwargs
+ )
deadline = time.time() + 10
while (time.time() < deadline):
@@ -1363,14 +1519,10 @@ class Node(object):
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):
@@ -1391,8 +1543,6 @@ class Node(object):
# 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)
@@ -1404,17 +1554,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")
@@ -1423,6 +1568,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:
@@ -1467,7 +1614,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:
@@ -1485,7 +1633,9 @@ class Node(object):
raise PartitionInUseError(
"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):
""" Get the next priority """
@@ -1501,15 +1651,4 @@ 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/tasks.py b/cxmanage_api/tasks.py
index 6b5cfde..7ed7851 100644
--- a/cxmanage_api/tasks.py
+++ b/cxmanage_api/tasks.py
@@ -43,7 +43,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 +51,7 @@ class Task(object):
self._method = method
self._args = args
+ self._kwargs = kwargs
self._finished = Event()
def join(self):
@@ -70,7 +71,7 @@ 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
@@ -96,7 +97,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 +111,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:
diff --git a/cxmanage_api/ubootenv.py b/cxmanage_api/ubootenv.py
index b5b8272..cd1a35a 100644
--- a/cxmanage_api/ubootenv.py
+++ b/cxmanage_api/ubootenv.py
@@ -33,7 +33,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
@@ -87,7 +87,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 +103,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":
@@ -159,7 +159,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 +171,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 +198,7 @@ class UbootEnv:
boot_args.append("reset")
break
else:
- raise UnknownBootCmdError("Unrecognized boot command: %s"
+ raise UbootenvError("Unrecognized boot command: %s"
% command)
if retry:
@@ -211,6 +211,64 @@ class UbootEnv:
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
+
+ commands = []
+ retry = False
+ reset = False
+
+ 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.
@@ -253,3 +311,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/fabric_test.py b/cxmanage_test/fabric_test.py
index fb234c5..f2720c9 100644
--- a/cxmanage_test/fabric_test.py
+++ b/cxmanage_test/fabric_test.py
@@ -140,6 +140,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()
@@ -347,6 +359,73 @@ 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 """
def __init__(self, ip_address, username="admin", password="admin",
@@ -405,6 +484,13 @@ class DummyNode(object):
self.executed.append("get_boot_order")
return ["disk", "pxe"]
+ def set_pxe_interface(self, interface):
+ self.executed.append(("set_pxe_interface", interface))
+
+ def get_pxe_interface(self):
+ self.executed.append("get_pxe_interface")
+ return "eth0"
+
def get_versions(self):
self.executed.append("get_versions")
diff --git a/cxmanage_test/node_test.py b/cxmanage_test/node_test.py
index d5d9445..b6f860b 100644
--- a/cxmanage_test/node_test.py
+++ b/cxmanage_test/node_test.py
@@ -61,6 +61,12 @@ class NodeTest(unittest.TestCase):
ipretriever=DummyIPRetriever, verbose=True)
for ip in ADDRESSES]
+ # 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-")
@@ -270,6 +276,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:
@@ -797,6 +847,20 @@ 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')
diff --git a/scripts/cxmanage b/scripts/cxmanage
index 101b30b..ccde835 100755
--- a/scripts/cxmanage
+++ b/scripts/cxmanage
@@ -42,14 +42,16 @@ 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.config import config_reset_command, config_boot_command, \
+ config_pxe_command
from cxmanage.commands.info import info_command
from cxmanage.commands.ipmitool import ipmitool_command
from cxmanage.commands.ipdiscover import ipdiscover_command
+from cxmanage.commands.tspackage import tspackage_command
-PYIPMI_VERSION = '0.7.1'
-IPMITOOL_VERSION = '1.8.11.0-cx5'
+PYIPMI_VERSION = '0.8.0'
+IPMITOOL_VERSION = '1.8.11.0-cx7'
PARSER_EPILOG = """examples:
@@ -195,6 +197,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',
@@ -248,6 +251,11 @@ def build_parser():
type=lambda x: [] if x == 'none' else x.split(','))
boot.set_defaults(func=config_boot_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='?',
@@ -284,6 +292,10 @@ def build_parser():
parser.add_argument('hostname',
help='nodes to operate on (see examples below)')
+ # tspackage command
+ tspackage = subparsers.add_parser('tspackage', help='Get all data from each node')
+ tspackage.set_defaults(func=tspackage_command)
+
return parser
diff --git a/setup.py b/setup.py
index bd49b13..4e2f47d 100644
--- a/setup.py
+++ b/setup.py
@@ -33,7 +33,7 @@ from setuptools import setup
setup(
name='cxmanage',
- version='0.8.2',
+ version='0.9.0',
packages=['cxmanage', 'cxmanage.commands', 'cxmanage_api'],
scripts=['scripts/cxmanage', 'scripts/sol_tabs'],
description='Calxeda Management Utility',
@@ -42,7 +42,7 @@ setup(
install_requires=[
'tftpy',
'pexpect',
- 'pyipmi>=0.7.1',
+ 'pyipmi>=0.8.0',
'argparse',
],
extras_require={