diff options
-rw-r--r-- | cxmanage/__init__.py | 13 | ||||
-rw-r--r-- | cxmanage/commands/config.py | 48 | ||||
-rw-r--r-- | cxmanage/commands/fw.py | 3 | ||||
-rw-r--r-- | cxmanage/commands/info.py | 12 | ||||
-rw-r--r-- | cxmanage/commands/tspackage.py | 369 | ||||
-rw-r--r-- | cxmanage_api/cx_exceptions.py | 44 | ||||
-rw-r--r-- | cxmanage_api/docs/source/index.rst | 1 | ||||
-rw-r--r-- | cxmanage_api/fabric.py | 225 | ||||
-rw-r--r-- | cxmanage_api/image.py | 2 | ||||
-rw-r--r-- | cxmanage_api/loggers.py | 397 | ||||
-rw-r--r-- | cxmanage_api/node.py | 465 | ||||
-rw-r--r-- | cxmanage_api/tasks.py | 9 | ||||
-rw-r--r-- | cxmanage_api/ubootenv.py | 76 | ||||
-rw-r--r-- | cxmanage_test/fabric_test.py | 86 | ||||
-rw-r--r-- | cxmanage_test/node_test.py | 64 | ||||
-rwxr-xr-x | scripts/cxmanage | 18 | ||||
-rw-r--r-- | setup.py | 4 |
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 @@ -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={ |