summaryrefslogtreecommitdiff
path: root/cxmanage
diff options
context:
space:
mode:
Diffstat (limited to 'cxmanage')
-rw-r--r--cxmanage/__init__.py324
-rw-r--r--cxmanage/commands/__init__.py29
-rw-r--r--cxmanage/commands/config.py94
-rw-r--r--cxmanage/commands/fabric.py80
-rw-r--r--cxmanage/commands/fw.py164
-rw-r--r--cxmanage/commands/info.py103
-rw-r--r--cxmanage/commands/ipdiscover.py56
-rw-r--r--cxmanage/commands/ipmitool.py60
-rw-r--r--cxmanage/commands/mc.py47
-rw-r--r--cxmanage/commands/power.py110
-rw-r--r--cxmanage/commands/sensor.py83
11 files changed, 1150 insertions, 0 deletions
diff --git a/cxmanage/__init__.py b/cxmanage/__init__.py
new file mode 100644
index 0000000..50b760a
--- /dev/null
+++ b/cxmanage/__init__.py
@@ -0,0 +1,324 @@
+# Copyright (c) 2012, Calxeda Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Calxeda Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+import sys
+import time
+
+from cxmanage_api.tftp import InternalTftp, ExternalTftp
+from cxmanage_api.node import Node
+from cxmanage_api.tasks import TaskQueue
+from cxmanage_api.cx_exceptions import TftpException
+
+
+def get_tftp(args):
+ """Get a TFTP server"""
+ if args.internal_tftp:
+ tftp_args = args.internal_tftp.split(':')
+ if len(tftp_args) == 1:
+ ip_address = tftp_args[0]
+ port = 0
+ elif len(tftp_args) == 2:
+ ip_address = tftp_args[0]
+ port = int(tftp_args[1])
+ else:
+ print ('ERROR: %s is not a valid argument for --internal-tftp'
+ % args.internal_tftp)
+ sys.exit(1)
+ return InternalTftp(ip_address=ip_address, port=port,
+ verbose=args.verbose)
+
+ elif args.external_tftp:
+ tftp_args = args.external_tftp.split(':')
+ if len(tftp_args) == 1:
+ ip_address = tftp_args[0]
+ port = 69
+ elif len(tftp_args) == 2:
+ ip_address = tftp_args[0]
+ port = int(tftp_args[1])
+ else:
+ print ('ERROR: %s is not a valid argument for --external-tftp'
+ % args.external_tftp)
+ sys.exit(1)
+ return ExternalTftp(ip_address=ip_address, port=port,
+ verbose=args.verbose)
+
+ return InternalTftp(verbose=args.verbose)
+
+
+def get_nodes(args, tftp, verify_prompt=False):
+ """Get nodes"""
+ hosts = []
+ for entry in args.hostname.split(','):
+ hosts.extend(parse_host_entry(entry))
+
+ nodes = [Node(ip_address=x, username=args.user, password=args.password,
+ tftp=tftp, ecme_tftp_port=args.ecme_tftp_port,
+ verbose=args.verbose) for x in hosts]
+
+ if args.all_nodes:
+ if not args.quiet:
+ print "Getting IP addresses..."
+
+ results, errors = run_command(args, nodes, "get_fabric_ipinfo")
+
+ all_nodes = []
+ for node in nodes:
+ if node in results:
+ for node_id, ip_address in sorted(results[node].iteritems()):
+ # TODO: make this more efficient. We can use a set of IP
+ # addresses instead of searching a list every time...
+ new_node = Node(ip_address=ip_address, username=args.user,
+ password=args.password, tftp=tftp,
+ ecme_tftp_port=args.ecme_tftp_port,
+ verbose=args.verbose)
+ new_node.node_id = node_id
+ if not new_node in all_nodes:
+ all_nodes.append(new_node)
+
+ node_strings = get_node_strings(args, all_nodes, justify=False)
+ if not args.quiet and all_nodes:
+ print "Discovered the following IP addresses:"
+ for node in all_nodes:
+ print node_strings[node]
+ print
+
+ if errors:
+ print "ERROR: Failed to get IP addresses. Aborting.\n"
+ sys.exit(1)
+
+ if args.nodes:
+ if len(all_nodes) != args.nodes:
+ print ("ERROR: Discovered %i nodes, expected %i. Aborting.\n"
+ % (len(all_nodes), args.nodes))
+ sys.exit(1)
+ elif verify_prompt and not args.force:
+ print "NOTE: Please check node count! Ensure discovery of all nodes in the cluster."
+ print "Power cycle your system if the discovered node count does not equal nodes in"
+ print "your system.\n"
+ if not prompt_yes("Discovered %i nodes. Continue?"
+ % len(all_nodes)):
+ sys.exit(1)
+
+ return all_nodes
+
+ return nodes
+
+
+def get_node_strings(args, nodes, justify=False):
+ """ Get string representations for the nodes. """
+ # Use the private _node_id instead of node_id. Strange choice,
+ # but we want to avoid accidentally polling the BMC.
+ if args.ids and all(x._node_id != None for x in nodes):
+ strings = ["Node %i (%s)" % (x._node_id, x.ip_address) for x in nodes]
+ else:
+ strings = [x.ip_address for x in nodes]
+
+ if justify:
+ just_size = max(16, max(len(x) for x in strings) + 1)
+ strings = [x.ljust(just_size) for x in strings]
+
+ return dict(zip(nodes, strings))
+
+
+def run_command(args, nodes, name, *method_args):
+ if args.threads != None:
+ task_queue = TaskQueue(threads=args.threads, delay=args.command_delay)
+ else:
+ task_queue = TaskQueue(delay=args.command_delay)
+
+ tasks = {}
+ for node in nodes:
+ tasks[node] = task_queue.put(getattr(node, name), *method_args)
+
+ results = {}
+ errors = {}
+ try:
+ counter = 0
+ while any(x.is_alive() for x in tasks.values()):
+ if not args.quiet:
+ _print_command_status(tasks, counter)
+ counter += 1
+ time.sleep(0.25)
+
+ for node, task in tasks.iteritems():
+ if task.status == "Completed":
+ results[node] = task.result
+ else:
+ errors[node] = task.error
+
+ except KeyboardInterrupt:
+ args.retry = 0
+
+ for node, task in tasks.iteritems():
+ if task.status == "Completed":
+ results[node] = task.result
+ elif task.status == "Failed":
+ errors[node] = task.error
+ else:
+ errors[node] = KeyboardInterrupt("Aborted by keyboard interrupt")
+
+ if not args.quiet:
+ _print_command_status(tasks, counter)
+ print "\n"
+
+ # Handle errors
+ should_retry = False
+ if errors:
+ _print_errors(args, nodes, errors)
+ if args.retry == None:
+ sys.stdout.write("Retry command on failed hosts? (y/n): ")
+ sys.stdout.flush()
+ while True:
+ command = raw_input().strip().lower()
+ if command in ['y', 'yes']:
+ should_retry = True
+ break
+ elif command in ['n', 'no']:
+ print
+ break
+ elif args.retry >= 1:
+ should_retry = True
+ if args.retry == 1:
+ print "Retrying command 1 more time..."
+ elif args.retry > 1:
+ print "Retrying command %i more times..." % args.retry
+ args.retry -= 1
+
+ if should_retry:
+ nodes = [x for x in nodes if x in errors]
+ new_results, errors = run_command(args, nodes, name, *method_args)
+ results.update(new_results)
+
+ return results, errors
+
+
+def prompt_yes(prompt):
+ sys.stdout.write("%s (y/n) " % prompt)
+ sys.stdout.flush()
+ while True:
+ command = raw_input().strip().lower()
+ if command in ['y', 'yes']:
+ print
+ return True
+ elif command in ['n', 'no']:
+ print
+ return False
+
+
+def parse_host_entry(entry, hostfiles=set()):
+ """parse a host entry"""
+ try:
+ return parse_hostfile_entry(entry, hostfiles)
+ except ValueError:
+ try:
+ return parse_ip_range_entry(entry)
+ except ValueError:
+ return [entry]
+
+
+def parse_hostfile_entry(entry, hostfiles=set()):
+ """parse a hostfile entry, returning a list of hosts"""
+ if entry.startswith('file='):
+ filename = entry[5:]
+ elif entry.startswith('hostfile='):
+ filename = entry[9:]
+ else:
+ raise ValueError('%s is not a hostfile entry' % entry)
+
+ if filename in hostfiles:
+ return []
+ hostfiles.add(filename)
+
+ entries = []
+ try:
+ for line in open(filename):
+ for element in line.partition('#')[0].split():
+ for hostfile_entry in element.split(','):
+ entries.extend(parse_host_entry(hostfile_entry, hostfiles))
+ except IOError:
+ print 'ERROR: %s is not a valid hostfile entry' % entry
+ sys.exit(1)
+
+ return entries
+
+
+def parse_ip_range_entry(entry):
+ """ Get a list of ip addresses in a given range"""
+ try:
+ start, end = entry.split('-')
+
+ # Convert start address to int
+ start_bytes = map(int, start.split('.'))
+ start_i = ((start_bytes[0] << 24) | (start_bytes[1] << 16)
+ | (start_bytes[2] << 8) | (start_bytes[3]))
+
+ # Convert end address to int
+ end_bytes = map(int, end.split('.'))
+ end_i = ((end_bytes[0] << 24) | (end_bytes[1] << 16)
+ | (end_bytes[2] << 8) | (end_bytes[3]))
+
+ # Get ip addresses in range
+ addresses = []
+ for i in range(start_i, end_i + 1):
+ address_bytes = [(i >> (24 - 8 * x)) & 0xff for x in range(4)]
+ addresses.append('%i.%i.%i.%i' % tuple(address_bytes))
+
+ except (ValueError, IndexError):
+ raise ValueError('%s is not an IP range' % entry)
+
+ return addresses
+
+
+def _print_errors(args, nodes, errors):
+ """ Print errors if they occured """
+ if errors:
+ node_strings = get_node_strings(args, nodes, justify=True)
+ print "Command failed on these hosts"
+ for node in nodes:
+ if node in errors:
+ print "%s: %s" % (node_strings[node], errors[node])
+ print
+
+ # Print a special message for TFTP errors
+ if all(isinstance(x, TftpException) for x in errors.itervalues()):
+ print "There may be networking issues (when behind NAT) between the host (where"
+ print "cxmanage is running) and the Calxeda node when establishing a TFTP session."
+ print "Please refer to the documentation for more information.\n"
+
+
+def _print_command_status(tasks, counter):
+ """ Print the status of a command """
+ message = "\r%i successes | %i errors | %i nodes left | %s"
+ successes = len([x for x in tasks.values() if x.status == "Completed"])
+ errors = len([x for x in tasks.values() if x.status == "Failed"])
+ nodes_left = len(tasks) - successes - errors
+ dots = "".join(["." for x in range(counter % 4)]).ljust(3)
+ sys.stdout.write(message % (successes, errors, nodes_left, dots))
+ sys.stdout.flush()
diff --git a/cxmanage/commands/__init__.py b/cxmanage/commands/__init__.py
new file mode 100644
index 0000000..2160043
--- /dev/null
+++ b/cxmanage/commands/__init__.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2012, Calxeda Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Calxeda Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
diff --git a/cxmanage/commands/config.py b/cxmanage/commands/config.py
new file mode 100644
index 0000000..ca80928
--- /dev/null
+++ b/cxmanage/commands/config.py
@@ -0,0 +1,94 @@
+# Copyright (c) 2012, Calxeda Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Calxeda Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+from cxmanage import get_tftp, get_nodes, get_node_strings, run_command
+
+from cxmanage_api.ubootenv import UbootEnv, validate_boot_args
+
+
+def config_reset_command(args):
+ """reset to factory default settings"""
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp, verify_prompt=True)
+
+ if not args.quiet:
+ print "Sending config reset command..."
+
+ results, errors = run_command(args, nodes, "config_reset")
+
+ if not args.quiet and not errors:
+ print "Command completed successfully.\n"
+
+ return len(errors) > 0
+
+
+def config_boot_command(args):
+ """set A9 boot order"""
+ if args.boot_order == ['status']:
+ return config_boot_status_command(args)
+
+ validate_boot_args(args.boot_order)
+
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print "Setting boot order..."
+
+ results, errors = run_command(args, nodes, "set_boot_order",
+ args.boot_order)
+
+ if not args.quiet and not errors:
+ print "Command completed successfully.\n"
+
+ return len(errors) > 0
+
+
+def config_boot_status_command(args):
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print "Getting boot order..."
+ results, errors = run_command(args, nodes, "get_boot_order")
+
+ # Print results
+ if results:
+ node_strings = get_node_strings(args, results, justify=True)
+ print "Boot order"
+ for node in nodes:
+ if node in results:
+ print "%s: %s" % (node_strings[node], ",".join(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/fabric.py b/cxmanage/commands/fabric.py
new file mode 100644
index 0000000..3bf84c2
--- /dev/null
+++ b/cxmanage/commands/fabric.py
@@ -0,0 +1,80 @@
+# Copyright (c) 2012, Calxeda Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Calxeda Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+from cxmanage import get_tftp, get_nodes, run_command
+
+
+def ipinfo_command(args):
+ """get ip info from a cluster or host"""
+ args.all_nodes = False
+
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print "Getting IP addresses..."
+
+ results, errors = run_command(args, nodes, "get_fabric_ipinfo")
+
+ for node in nodes:
+ if node in results:
+ print 'IP info from %s' % node.ip_address
+ for node_id, node_address in results[node].iteritems():
+ print 'Node %i: %s' % (node_id, node_address)
+ print
+
+ return 0
+
+
+def macaddrs_command(args):
+ """get mac addresses from a cluster or host"""
+ args.all_nodes = False
+
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print "Getting MAC addresses..."
+ results, errors = run_command(args, nodes, "get_fabric_macaddrs")
+
+ for node in nodes:
+ if node in results:
+ print "MAC addresses from %s" % node.ip_address
+ for node_id in results[node]:
+ for port in results[node][node_id]:
+ for mac_address in results[node][node_id][port]:
+ print "Node %i, Port %i: %s" % (node_id, port,
+ mac_address)
+ 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
new file mode 100644
index 0000000..87f810b
--- /dev/null
+++ b/cxmanage/commands/fw.py
@@ -0,0 +1,164 @@
+# Copyright (c) 2012, Calxeda Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Calxeda Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+from pkg_resources import parse_version
+
+from cxmanage import get_tftp, get_nodes, get_node_strings, run_command, \
+ prompt_yes
+
+from cxmanage_api.image import Image
+from cxmanage_api.firmware_package import FirmwarePackage
+
+
+def fwupdate_command(args):
+ """update firmware on a cluster or host"""
+ def do_update():
+ """ Do a single firmware check+update. Returns True on failure. """
+ if not args.force:
+ if not args.quiet:
+ print "Checking hosts..."
+
+ results, errors = run_command(args, nodes, "_check_firmware",
+ package, args.partition, args.priority)
+ if errors:
+ print "ERROR: Firmware update aborted."
+ return True
+
+ if not args.quiet:
+ print "Updating firmware..."
+
+ results, errors = run_command(args, nodes, "update_firmware", package,
+ args.partition, args.priority)
+ if errors:
+ print "ERROR: Firmware update failed."
+ return True
+
+ return False
+
+ def do_reset():
+ """ Reset and wait. Returns True on failure. """
+ if not args.quiet:
+ print "Checking ECME versions..."
+
+ results, errors = run_command(args, nodes, "get_versions")
+ if errors:
+ print "ERROR: MC reset aborted. Backup partitions not updated."
+ return True
+
+ for result in results.values():
+ version = result.ecme_version.lstrip("v")
+ if parse_version(version) < parse_version("1.2.0"):
+ print "ERROR: MC reset is unsafe on ECME version v%s" % version
+ print "Please power cycle the system and start a new fwupdate."
+ return True
+
+ if not args.quiet:
+ print "Resetting nodes..."
+
+ results, errors = run_command(args, nodes, "mc_reset", True)
+ if errors:
+ print "ERROR: MC reset failed. Backup partitions not updated."
+ return True
+
+ return False
+
+ if args.image_type == "PACKAGE":
+ package = FirmwarePackage(args.filename)
+ else:
+ try:
+ simg = None
+ if args.force_simg:
+ simg = False
+ elif args.skip_simg:
+ simg = True
+
+ image = Image(args.filename, args.image_type, simg, args.daddr,
+ args.skip_crc32, args.fw_version)
+ package = FirmwarePackage()
+ package.images.append(image)
+ except ValueError as e:
+ print "ERROR: %s" % e
+ return True
+
+ if not args.all_nodes:
+ if args.force:
+ print 'WARNING: Updating firmware without --all-nodes is dangerous.'
+ else:
+ if not prompt_yes(
+ 'WARNING: Updating firmware without --all-nodes is dangerous. Continue?'):
+ return 1
+
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp, verify_prompt=True)
+
+ errors = do_update()
+
+ if args.full and not errors:
+ errors = do_reset()
+ if not errors:
+ errors = do_update()
+
+ if not args.quiet and not errors:
+ print "Command completed successfully.\n"
+
+ return errors
+
+
+def fwinfo_command(args):
+ """print firmware info"""
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print "Getting firmware info..."
+
+ results, errors = run_command(args, nodes, "get_firmware_info")
+
+ node_strings = get_node_strings(args, results, justify=False)
+ for node in nodes:
+ if node in results:
+ print "[ Firmware info for %s ]" % node_strings[node]
+
+ for partition in results[node]:
+ print "Partition : %s" % partition.partition
+ print "Type : %s" % partition.type
+ print "Offset : %s" % partition.offset
+ print "Size : %s" % partition.size
+ print "Priority : %s" % partition.priority
+ print "Daddr : %s" % partition.daddr
+ print "Flags : %s" % partition.flags
+ print "Version : %s" % partition.version
+ print "In Use : %s" % partition.in_use
+ print
+
+ if not args.quiet and errors:
+ print "Some errors occured during the command.\n"
+
+ return len(errors) > 0
diff --git a/cxmanage/commands/info.py b/cxmanage/commands/info.py
new file mode 100644
index 0000000..d002906
--- /dev/null
+++ b/cxmanage/commands/info.py
@@ -0,0 +1,103 @@
+# Copyright (c) 2012, Calxeda Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Calxeda Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+from cxmanage import get_tftp, get_nodes, get_node_strings, run_command
+
+
+def info_command(args):
+ """print info from a cluster or host"""
+ if args.info_type in [None, 'basic']:
+ return info_basic_command(args)
+ elif args.info_type == 'ubootenv':
+ return info_ubootenv_command(args)
+
+
+def info_basic_command(args):
+ """Print basic info"""
+ components = [
+ ("ecme_version", "ECME version"),
+ ("cdb_version", "CDB version"),
+ ("stage2_version", "Stage2boot version"),
+ ("bootlog_version", "Bootlog version"),
+ ("a9boot_version", "A9boot version"),
+ ("uboot_version", "Uboot version"),
+ ("ubootenv_version", "Ubootenv version"),
+ ("dtb_version", "DTB version")
+ ]
+
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print "Getting info..."
+ results, errors = run_command(args, nodes, "get_versions")
+
+ # Print results
+ node_strings = get_node_strings(args, results, justify=False)
+ for node in nodes:
+ if node in results:
+ result = results[node]
+ print "[ Info from %s ]" % node_strings[node]
+ print "Hardware version : %s" % result.hardware_version
+ print "Firmware version : %s" % result.firmware_version
+ for var, string in components:
+ if hasattr(result, var):
+ version = getattr(result, var)
+ print "%s: %s" % (string.ljust(19), version)
+ print
+
+ if not args.quiet and errors:
+ print "Some errors occured during the command.\n"
+
+ return len(errors) > 0
+
+
+def info_ubootenv_command(args):
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print "Getting u-boot environment..."
+ results, errors = run_command(args, nodes, "get_ubootenv")
+
+ # Print results
+ node_strings = get_node_strings(args, results, justify=False)
+ for node in nodes:
+ if node in results:
+ ubootenv = results[node]
+ print "[ U-Boot Environment from %s ]" % node_strings[node]
+ for variable in ubootenv.variables:
+ print "%s=%s" % (variable, ubootenv.variables[variable])
+ print
+
+ if not args.quiet and errors:
+ print "Some errors occured during the command.\n"
+
+ return len(errors) > 0
diff --git a/cxmanage/commands/ipdiscover.py b/cxmanage/commands/ipdiscover.py
new file mode 100644
index 0000000..f619d16
--- /dev/null
+++ b/cxmanage/commands/ipdiscover.py
@@ -0,0 +1,56 @@
+# Copyright (c) 2012, Calxeda Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Calxeda Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+from cxmanage import get_tftp, get_nodes, get_node_strings, run_command
+
+
+def ipdiscover_command(args):
+ """discover server IP addresses"""
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print 'Getting server-side IP addresses...'
+
+ results, errors = run_command(args, nodes, 'get_server_ip', args.interface,
+ args.ipv6, args.server_user, args.server_password, args.aggressive)
+
+ if results:
+ node_strings = get_node_strings(args, results, justify=True)
+ print 'IP addresses (ECME, Server)'
+ 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 occurred during the command.'
+
+ return len(errors) > 0
diff --git a/cxmanage/commands/ipmitool.py b/cxmanage/commands/ipmitool.py
new file mode 100644
index 0000000..f8baf80
--- /dev/null
+++ b/cxmanage/commands/ipmitool.py
@@ -0,0 +1,60 @@
+# Copyright (c) 2012, Calxeda Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Calxeda Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+from cxmanage import get_tftp, get_nodes, get_node_strings, run_command
+
+
+def ipmitool_command(args):
+ """run arbitrary ipmitool command"""
+ if args.lanplus:
+ ipmitool_args = ['-I', 'lanplus'] + args.ipmitool_args
+ else:
+ ipmitool_args = args.ipmitool_args
+
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print "Running IPMItool command..."
+ results, errors = run_command(args, nodes, "ipmitool_command",
+ ipmitool_args)
+
+ # Print results
+ node_strings = get_node_strings(args, results, justify=False)
+ for node in nodes:
+ if node in results and results[node] != "":
+ print "[ IPMItool output from %s ]" % node_strings[node]
+ print 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/mc.py b/cxmanage/commands/mc.py
new file mode 100644
index 0000000..2573540
--- /dev/null
+++ b/cxmanage/commands/mc.py
@@ -0,0 +1,47 @@
+# Copyright (c) 2012, Calxeda Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Calxeda Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+from cxmanage import get_tftp, get_nodes, run_command
+
+
+def mcreset_command(args):
+ """reset the management controllers of a cluster or host"""
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print 'Sending MC reset command...'
+
+ results, errors = run_command(args, nodes, 'mc_reset')
+
+ if not args.quiet and not errors:
+ print 'Command completed successfully.\n'
+
+ return len(errors) > 0
diff --git a/cxmanage/commands/power.py b/cxmanage/commands/power.py
new file mode 100644
index 0000000..b5b6015
--- /dev/null
+++ b/cxmanage/commands/power.py
@@ -0,0 +1,110 @@
+# Copyright (c) 2012, Calxeda Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Calxeda Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+from cxmanage import get_tftp, get_nodes, get_node_strings, run_command
+
+
+def power_command(args):
+ """change the power state of a cluster or host"""
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print 'Sending power %s command...' % args.power_mode
+
+ results, errors = run_command(args, nodes, 'set_power', args.power_mode)
+
+ if not args.quiet and not errors:
+ print 'Command completed successfully.\n'
+
+ return len(errors) > 0
+
+
+def power_status_command(args):
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print 'Getting power status...'
+ results, errors = run_command(args, nodes, 'get_power')
+
+ # Print results
+ if results:
+ node_strings = get_node_strings(args, results, justify=True)
+ print 'Power status'
+ for node in nodes:
+ if node in results:
+ result = 'on' if results[node] else 'off'
+ print '%s: %s' % (node_strings[node], result)
+ print
+
+ if not args.quiet and errors:
+ print 'Some errors occured during the command.\n'
+
+ return len(errors) > 0
+
+
+def power_policy_command(args):
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print 'Setting power policy to %s...' % args.policy
+
+ results, errors = run_command(args, nodes, 'set_power_policy',
+ args.policy)
+
+ if not args.quiet and not errors:
+ print 'Command completed successfully.\n'
+
+ return len(errors) > 0
+
+
+def power_policy_status_command(args):
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print 'Getting power policy status...'
+ results, errors = run_command(args, nodes, 'get_power_policy')
+
+ # Print results
+ if results:
+ node_strings = get_node_strings(args, results, justify=True)
+ print 'Power policy status'
+ 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/sensor.py b/cxmanage/commands/sensor.py
new file mode 100644
index 0000000..c3fed32
--- /dev/null
+++ b/cxmanage/commands/sensor.py
@@ -0,0 +1,83 @@
+# Copyright (c) 2012, Calxeda Inc.
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Calxeda Inc. nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+from cxmanage import get_tftp, get_nodes, get_node_strings, run_command
+
+
+def sensor_command(args):
+ """read sensor values from a cluster or host"""
+ tftp = get_tftp(args)
+ nodes = get_nodes(args, tftp)
+
+ if not args.quiet:
+ print "Getting sensor readings..."
+ results, errors = run_command(args, nodes, "get_sensors",
+ args.sensor_name)
+
+ sensors = {}
+ for node in nodes:
+ 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, ""))
+
+ node_strings = get_node_strings(args, results, justify=True)
+ jsize = len(node_strings.itervalues().next())
+ for sensor_name, readings in sensors.iteritems():
+ print sensor_name
+
+ for node, reading, suffix in readings:
+ print "%s: %.2f %s" % (node_strings[node], reading, suffix)
+
+ try:
+ if all(suffix == x[2] for x in readings):
+ minimum = min(x[1] for x in readings)
+ maximum = max(x[1] for x in readings)
+ average = sum(x[1] for x in readings) / len(readings)
+ print "%s: %.2f %s" % ("Minimum".ljust(jsize), minimum, suffix)
+ print "%s: %.2f %s" % ("Maximum".ljust(jsize), maximum, suffix)
+ print "%s: %.2f %s" % ("Average".ljust(jsize), average, suffix)
+ except ValueError:
+ pass
+
+ print
+
+ if not args.quiet and errors:
+ print "Some errors occured during the command.\n"
+
+ return len(errors) > 0