From 7e1dea01472ce20c90dd69f11e6c07df9f1d6847 Mon Sep 17 00:00:00 2001 From: Lorry Tar Creator Date: Thu, 9 May 2013 18:09:52 +0000 Subject: Imported from /home/lorry/working-area/delta_cxmanage-tarball/cxmanage-0.8.2.tar.gz. --- PKG-INFO | 12 + cxmanage.egg-info/PKG-INFO | 12 + cxmanage.egg-info/SOURCES.txt | 31 + cxmanage.egg-info/dependency_links.txt | 1 + cxmanage.egg-info/requires.txt | 8 + cxmanage.egg-info/top_level.txt | 2 + cxmanage/__init__.py | 324 +++++++ cxmanage/commands/__init__.py | 29 + cxmanage/commands/config.py | 94 ++ cxmanage/commands/fabric.py | 80 ++ cxmanage/commands/fw.py | 164 ++++ cxmanage/commands/info.py | 103 +++ cxmanage/commands/ipdiscover.py | 56 ++ cxmanage/commands/ipmitool.py | 60 ++ cxmanage/commands/mc.py | 47 + cxmanage/commands/power.py | 110 +++ cxmanage/commands/sensor.py | 83 ++ cxmanage_api/__init__.py | 65 ++ cxmanage_api/crc32.py | 126 +++ cxmanage_api/cx_exceptions.py | 393 +++++++++ cxmanage_api/fabric.py | 904 +++++++++++++++++++ cxmanage_api/firmware_package.py | 168 ++++ cxmanage_api/image.py | 178 ++++ cxmanage_api/ip_retriever.py | 382 ++++++++ cxmanage_api/node.py | 1507 ++++++++++++++++++++++++++++++++ cxmanage_api/simg.py | 239 +++++ cxmanage_api/tasks.py | 175 ++++ cxmanage_api/tftp.py | 297 +++++++ cxmanage_api/ubootenv.py | 255 ++++++ scripts/cxmanage | 374 ++++++++ scripts/sol_tabs | 57 ++ setup.cfg | 5 + setup.py | 54 ++ 33 files changed, 6395 insertions(+) create mode 100644 PKG-INFO create mode 100644 cxmanage.egg-info/PKG-INFO create mode 100644 cxmanage.egg-info/SOURCES.txt create mode 100644 cxmanage.egg-info/dependency_links.txt create mode 100644 cxmanage.egg-info/requires.txt create mode 100644 cxmanage.egg-info/top_level.txt create mode 100644 cxmanage/__init__.py create mode 100644 cxmanage/commands/__init__.py create mode 100644 cxmanage/commands/config.py create mode 100644 cxmanage/commands/fabric.py create mode 100644 cxmanage/commands/fw.py create mode 100644 cxmanage/commands/info.py create mode 100644 cxmanage/commands/ipdiscover.py create mode 100644 cxmanage/commands/ipmitool.py create mode 100644 cxmanage/commands/mc.py create mode 100644 cxmanage/commands/power.py create mode 100644 cxmanage/commands/sensor.py create mode 100644 cxmanage_api/__init__.py create mode 100644 cxmanage_api/crc32.py create mode 100644 cxmanage_api/cx_exceptions.py create mode 100644 cxmanage_api/fabric.py create mode 100644 cxmanage_api/firmware_package.py create mode 100644 cxmanage_api/image.py create mode 100644 cxmanage_api/ip_retriever.py create mode 100644 cxmanage_api/node.py create mode 100644 cxmanage_api/simg.py create mode 100644 cxmanage_api/tasks.py create mode 100644 cxmanage_api/tftp.py create mode 100644 cxmanage_api/ubootenv.py create mode 100755 scripts/cxmanage create mode 100755 scripts/sol_tabs create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..8a8c7a2 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,12 @@ +Metadata-Version: 1.1 +Name: cxmanage +Version: 0.8.2 +Summary: Calxeda Management Utility +Home-page: UNKNOWN +Author: UNKNOWN +Author-email: UNKNOWN +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python :: 2.7 diff --git a/cxmanage.egg-info/PKG-INFO b/cxmanage.egg-info/PKG-INFO new file mode 100644 index 0000000..8a8c7a2 --- /dev/null +++ b/cxmanage.egg-info/PKG-INFO @@ -0,0 +1,12 @@ +Metadata-Version: 1.1 +Name: cxmanage +Version: 0.8.2 +Summary: Calxeda Management Utility +Home-page: UNKNOWN +Author: UNKNOWN +Author-email: UNKNOWN +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python :: 2.7 diff --git a/cxmanage.egg-info/SOURCES.txt b/cxmanage.egg-info/SOURCES.txt new file mode 100644 index 0000000..560f314 --- /dev/null +++ b/cxmanage.egg-info/SOURCES.txt @@ -0,0 +1,31 @@ +setup.py +cxmanage/__init__.py +cxmanage.egg-info/PKG-INFO +cxmanage.egg-info/SOURCES.txt +cxmanage.egg-info/dependency_links.txt +cxmanage.egg-info/requires.txt +cxmanage.egg-info/top_level.txt +cxmanage/commands/__init__.py +cxmanage/commands/config.py +cxmanage/commands/fabric.py +cxmanage/commands/fw.py +cxmanage/commands/info.py +cxmanage/commands/ipdiscover.py +cxmanage/commands/ipmitool.py +cxmanage/commands/mc.py +cxmanage/commands/power.py +cxmanage/commands/sensor.py +cxmanage_api/__init__.py +cxmanage_api/crc32.py +cxmanage_api/cx_exceptions.py +cxmanage_api/fabric.py +cxmanage_api/firmware_package.py +cxmanage_api/image.py +cxmanage_api/ip_retriever.py +cxmanage_api/node.py +cxmanage_api/simg.py +cxmanage_api/tasks.py +cxmanage_api/tftp.py +cxmanage_api/ubootenv.py +scripts/cxmanage +scripts/sol_tabs \ No newline at end of file diff --git a/cxmanage.egg-info/dependency_links.txt b/cxmanage.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/cxmanage.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/cxmanage.egg-info/requires.txt b/cxmanage.egg-info/requires.txt new file mode 100644 index 0000000..e810fff --- /dev/null +++ b/cxmanage.egg-info/requires.txt @@ -0,0 +1,8 @@ +tftpy +pexpect +pyipmi>=0.7.1 +argparse + +[docs] +sphinx +cloud_sptheme \ No newline at end of file diff --git a/cxmanage.egg-info/top_level.txt b/cxmanage.egg-info/top_level.txt new file mode 100644 index 0000000..26b8c34 --- /dev/null +++ b/cxmanage.egg-info/top_level.txt @@ -0,0 +1,2 @@ +cxmanage +cxmanage_api 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 diff --git a/cxmanage_api/__init__.py b/cxmanage_api/__init__.py new file mode 100644 index 0000000..2228b38 --- /dev/null +++ b/cxmanage_api/__init__.py @@ -0,0 +1,65 @@ +# 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 os +import atexit +import shutil +import tempfile + + +WORK_DIR = tempfile.mkdtemp(prefix="cxmanage_api-") +atexit.register(lambda: shutil.rmtree(WORK_DIR)) + + +def temp_file(): + """ + Create a temporary file that will be cleaned up at exit. + + :returns: File name of the temporary file created. + :rtype: string + + """ + fd, filename = tempfile.mkstemp(dir=WORK_DIR) + os.close(fd) + return filename + +def temp_dir(): + """ + Create a temporary directory that will be cleaned up at exit. + + :returns: Path to the temporary directory created. + :rtype: string + + """ + return tempfile.mkdtemp(dir=WORK_DIR) + + +# End of file:./__init__.py diff --git a/cxmanage_api/crc32.py b/cxmanage_api/crc32.py new file mode 100644 index 0000000..aca7838 --- /dev/null +++ b/cxmanage_api/crc32.py @@ -0,0 +1,126 @@ +# 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. + + +""" +This is a python implementation of freebsd's ssh/crc32.c. +Written in python for convenient use in the cxmanage script. +""" + +TABLE = [0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, + 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, + 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, + 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, + 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, + 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, + 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, + 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, + 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, + 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, + 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, + 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, + 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, + 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, + 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, + 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, + 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, + 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, + 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, + 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, + 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, + 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, + 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, + 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, + 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, + 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, + 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, + 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, + 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, + 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, + 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, + 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, + 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, + 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, + 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, + 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, + 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, + 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, + 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, + 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, + 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, + 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, + 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, + 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, + 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, + 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, + 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, + 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, + 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, + 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, + 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d] + +def get_crc32(string, crc=0): + """Computes the crc32 value of the given string. + + >>> from cxmanage_api.crc32 import get_crc32 + >>> get_crc32(string='Foo Bar Baz') + 3901333286 + >>> # + >>> # With an optional offset ... + >>> # + >>> get_crc32(string='Foo Bar Baz', crc=1) + 688341222 + + :param string: The string to calculate the crc32 for. + :type string: string + :param crc: The XOR offset. + :type crc: integer + + """ + for char in string: + byte = ord(char) + crc = TABLE[(crc ^ byte) & 0xff] ^ (crc >> 8) + return crc + + +# End of file: ./crc32.py diff --git a/cxmanage_api/cx_exceptions.py b/cxmanage_api/cx_exceptions.py new file mode 100644 index 0000000..410b5d7 --- /dev/null +++ b/cxmanage_api/cx_exceptions.py @@ -0,0 +1,393 @@ +# 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. + +"""Defines the custom exceptions used by the cxmanage_api project.""" + +from pyipmi import IpmiError +from tftpy.TftpShared import TftpException + + +class TimeoutError(Exception): + """Raised when a timeout has been reached. + + >>> from cxmanage_api.cx_exceptions import TimeoutError + >>> raise TimeoutError('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.TimeoutError: My custom exception text! + + :param msg: Exceptions message and details to return to the user. + :type msg: string + :raised: When a timeout has been reached. + + """ + + def __init__(self, msg): + """Default constructor for the TimoutError class.""" + super(TimeoutError, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + return self.msg + + +class NoPartitionError(Exception): + """Raised when a partition is not found. + + >>> from cxmanage_api.cx_exceptions import NoPartitionError + >>> raise NoPartitionError('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.NoPartitionError: My custom exception text! + + :param msg: Exceptions message and details to return to the user. + :type msg: string + :raised: When a partition is not found. + + """ + + def __init__(self, msg): + """Default constructor for the NoPartitionError class.""" + super(NoPartitionError, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + return self.msg + + +class NoSensorError(Exception): + """Raised when a sensor or sensors are not found. + + >>> from cxmanage_api.cx_exceptions import NoSensorError + >>> raise NoSensorError('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.NoSensorError: My custom exception text! + + :param msg: Exceptions message and details to return to the user. + :type msg: string + :raised: When a sensor or sensors are not found. + + """ + + def __init__(self, msg): + """Default constructor for the NoSensorError class.""" + super(NoSensorError, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + 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 "", line 1, in + 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. + + >>> from cxmanage_api.cx_exceptions import SocmanVersionError + >>> raise SocmanVersionError('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.SocmanVersionError: My custom exception text! + + :param msg: Exceptions message and details to return to the user. + :type msg: string + :raised: When there is an error with the users socman version. + + """ + + def __init__(self, msg): + """Default constructor for the SocmanVersionError class.""" + super(SocmanVersionError, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + return self.msg + + +class FirmwareConfigError(Exception): + """Raised when there are slot/firmware version inconsistencies. + + >>> from cxmanage_api.cx_exceptions import FirmwareConfigError + >>> raise FirmwareConfigError('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.FirmwareConfigError: My custom exception text! + + :param msg: Exceptions message and details to return to the user. + :type msg: string + :raised: When there are slot/firmware version inconsistencies. + + """ + + def __init__(self, msg): + """Default constructor for the FirmwareConfigError class.""" + super(FirmwareConfigError, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + return self.msg + + +class PriorityIncrementError(Exception): + """Raised when the Priority on a SIMG image cannot be altered. + + >>> from cxmanage_api.cx_exceptions import PriorityIncrementError + >>> raise PriorityIncrementError('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.PriorityIncrementError: My custom exception text! + + :param msg: Exceptions message and details to return to the user. + :type msg: string + :raised: When the Priority on a SIMG image cannot be altered. + + """ + + def __init__(self, msg): + """Default constructor for the PriorityIncrementError class.""" + super(PriorityIncrementError, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + return self.msg + + +class ImageSizeError(Exception): + """Raised when the actual size of the image is not what is expected. + + >>> from cxmanage_api.cx_exceptions import ImageSizeError + >>> raise ImageSizeError('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.ImageSizeError: My custom exception text! + + :param msg: Exceptions message and details to return to the user. + :type msg: string + :raised: When the actual size of the image is not what is expected. + + """ + + def __init__(self, msg): + """Default constructor for the ImageSizeError class.""" + super(ImageSizeError, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + return self.msg + + +class TransferFailure(Exception): + """Raised when the transfer of a file has failed. + + >>> from cxmanage_api.cx_exceptions import TransferFailure + >>> raise TransferFailure('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.TransferFailure: My custom exception text! + + :param msg: Exceptions message and details to return to the user. + :type msg: string + :raised: When the transfer of a file has failed. + + """ + + def __init__(self, msg): + """Default constructor for the TransferFailure class.""" + super(TransferFailure, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + return self.msg + + +class InvalidImageError(Exception): + """Raised when an image is not valid. (i.e. fails verification). + + >>> from cxmanage_api.cx_exceptions import InvalidImageError + >>> raise InvalidImageError('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.InvalidImageError: My custom exception text! + + :param msg: Exceptions message and details to return to the user. + :type msg: string + :raised: When an image is not valid. (i.e. fails verification). + + """ + + def __init__(self, msg): + """Default constructor for the InvalidImageError class.""" + super(InvalidImageError, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + 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. + + >>> from cxmanage_api.cx_exceptions import UnknownBootCmdError + >>> raise UnknownBootCmdError('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.UnknownBootCmdError: 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. + + """ + + def __init__(self, msg): + """Default constructor for the UnknownBootCmdError class.""" + super(UnknownBootCmdError, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + return self.msg + + +class CommandFailedError(Exception): + """Raised when a command has failed. + + >>> from cxmanage_api.cx_exceptions import CommandFailedError + >>> raise CommandFailedError('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.CommandFailedError: My custom exception text! + + :param results: Command results. (map of nodes->results) + :type results: dictionary + :param errors: Command errors. (map of nodes->errors) + :type errors: dictionary + :raised: When a command has failed. + + """ + + def __init__(self, results, errors): + """Default constructor for the CommandFailedError class.""" + self.results = results + self.errors = errors + + def __repr__(self): + return 'Results: %s Errors: %s' % (self.results, self.errors) + + def __str__(self): + return str(dict((x, str(y)) for x, y in self.errors.iteritems())) + + +class PartitionInUseError(Exception): + """Raised when trying to upload to a CDB/BOOT_LOG partition that's in use. + + >>> from cxmanage_api.cx_exceptions import PartitionInUseError + >>> raise PartitionInUseError('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.PartitionInUseError: My custom exception text! + + :param msg: Exceptions message and details to return to the user. + :type msg: string + :raised: When trying to upload to a CDB/BOOT_LOG partition that's in use. + + """ + + def __init__(self, msg): + """Default constructor for the PartitionInUseError class.""" + super(PartitionInUseError, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + return self.msg + + +class IPDiscoveryError(Exception): + """Raised when server IP discovery fails for any reason. + + >>> from cxmanage_api.cx_exceptions import IPDiscoveryError + >>> raise IPDiscoveryError('My custom exception text!') + Traceback (most recent call last): + File "", line 1, in + cxmanage_api.cx_exceptions.IPDiscoveryError: My custom exception text! + + :param msg: Exceptions message and details to return to the user. + :type msg: string + :raised: When IP discovery fails for any reason. + + """ + + def __init__(self, msg): + """Default constructor for the IPDsicoveryError class.""" + super(IPDiscoveryError, self).__init__() + self.msg = msg + + def __str__(self): + """String representation of this Exception class.""" + return self.msg + + +# End of file: exceptions.py diff --git a/cxmanage_api/fabric.py b/cxmanage_api/fabric.py new file mode 100644 index 0000000..34f435e --- /dev/null +++ b/cxmanage_api/fabric.py @@ -0,0 +1,904 @@ +# 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_api.tasks import DEFAULT_TASK_QUEUE +from cxmanage_api.tftp import InternalTftp +from cxmanage_api.node import Node as NODE +from cxmanage_api.cx_exceptions import CommandFailedError + + +class Fabric(object): + """ The Fabric class provides management of multiple nodes. + + >>> from cxmanage_api.fabric import Fabric + >>> fabric = Fabric('10.20.1.9') + + :param ip_address: The ip_address of ANY known node for the Fabric. + :type ip_address: string + :param username: The login username credential. [Default admin] + :type username: string + :param password: The login password credential. [Default admin] + :type password: string + :param tftp: Tftp server to facilitate IPMI command responses. + :type tftp: `Tftp `_ + :param task_queue: TaskQueue to use for sending commands. + :type task_queue: `TaskQueue `_ + :param verbose: Flag to turn on verbose output (cmd/response). + :type verbose: boolean + :param node: Node type, for dependency integration. + :type node: `Node `_ + """ + + def __init__(self, ip_address, username="admin", password="admin", + tftp=None, ecme_tftp_port=5001, task_queue=None, + verbose=False, node=None): + """Default constructor for the Fabric class.""" + self.ip_address = ip_address + self.username = username + self.password = password + self._tftp = tftp + self.ecme_tftp_port = ecme_tftp_port + self.task_queue = task_queue + self.verbose = verbose + self.node = node + + self._nodes = {} + + if (not self.node): + self.node = NODE + + 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) + + def __hash__(self): + """__hash__() override.""" + return hash(tuple(self.nodes.iteritems())) + + def __str__(self): + """__str__() override.""" + return 'Fabric Node 0: %s (%d nodes)' % (self.nodes[0].ip_address, + len(self.nodes)) + + @property + def tftp(self): + """Returns the tftp server for this Fabric. + + >>> fabric.tftp + + + :return: The tftp server. + :rtype: `Tftp `_ + + """ + return self._tftp + + @tftp.setter + def tftp(self, value): + """ Set the TFTP server for this fabric (and all nodes) """ + self._tftp = value + + if not self._nodes: + return + + for node in self.nodes.values(): + node.tftp = value + + @property + def nodes(self): + """List of nodes in this fabric. + + >>> fabric.nodes + { + 0: , + 1: , + 2: , + 3: + } + + .. note:: + * Fabric nodes are lazily initialized. + + :returns: A mapping of node ids to node objects. + :rtype: dictionary + + """ + if not self._nodes: + self._discover_nodes(self.ip_address) + return self._nodes + + @property + def primary_node(self): + """The node to use for fabric config operations. + + Today, this is always node 0. + + >>> fabric.primary_node + + + :return: Node object for primary node + :rtype: Node object + """ + return self.nodes[0] + + def get_mac_addresses(self): + """Gets MAC addresses from all nodes. + + >>> fabric.get_mac_addresses() + { + 0: ['fc:2f:40:3b:ec:40', 'fc:2f:40:3b:ec:41', 'fc:2f:40:3b:ec:42'], + 1: ['fc:2f:40:91:dc:40', 'fc:2f:40:91:dc:41', 'fc:2f:40:91:dc:42'], + 2: ['fc:2f:40:ab:f7:14', 'fc:2f:40:ab:f7:15', 'fc:2f:40:ab:f7:16'], + 3: ['fc:2f:40:88:b3:6c', 'fc:2f:40:88:b3:6d', 'fc:2f:40:88:b3:6e'] + } + + :param async: Flag that determines if the command result (dictionary) + is returned or a Task object (can get status, etc.). + :type async: boolean + + :return: The MAC addresses for each node. + :rtype: dictionary + + """ + return self.primary_node.get_fabric_macaddrs() + + def get_uplink_info(self): + """Gets the fabric uplink info. + + >>> fabric.get_uplink_info() + { + 0: {0: 0, 1: 0, 2: 0} + 1: {0: 0, 1: 0, 2: 0} + 2: {0: 0, 1: 0, 2: 0} + 3: {0: 0, 1: 0, 2: 0} + } + + :param async: Flag that determines if the command result (dictionary) + is returned or a Task object (can get status, etc.). + :type async: boolean + + :return: The uplink info for each node. + :rtype: dictionary + + """ + return self.primary_node.get_fabric_uplink_info() + + def get_power(self, async=False): + """Returns the power status for all nodes. + + >>> fabric.get_power() + {0: False, 1: False, 2: False, 3: False} + + :param async: Flag that determines if the command result (dictionary) + is returned or a Command object (for cmd status, etc.). + :type async: boolean + + :return: The power status of each node. + :rtype: dictionary or `Task `__ + + """ + return self._run_on_all_nodes(async, "get_power") + + def set_power(self, mode, async=False): + """Send an IPMI power command to all nodes. + + >>> # On ... + >>> fabric.set_power(mode='on') + >>> # Off ... + >>> fabric.set_power(mode='off') + >>> # Sanity check ... + >>> fabric.get_power() + {0: False, 1: False, 2: False, 3: False} + + :param mode: Mode to set the power to (for all nodes). + :type mode: 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_power", mode) + + def get_power_policy(self, async=False): + """Gets the power policy from all nodes. + + >>> fabric.get_power_policy() + {0: 'always-on', 1: 'always-on', 2: 'always-on', 3: 'always-on'} + + :param async: Flag that determines if the command result (dictionary) + is returned or a Command object (can get status, etc.). + :type async: boolean + + :return: The power policy for all nodes on this fabric. + :rtype: dictionary or `Task `__ + + """ + return self._run_on_all_nodes(async, "get_power_policy") + + def set_power_policy(self, state, async=False): + """Sets the power policy on all nodes. + + >>> fabric.set_power_policy(state='always-off') + >>> # Check to see if it took ... + >>> fabric.get_power_policy() + {0: 'always-off', 1: 'always-off', 2: 'always-off', 3: 'always-off'} + + :param state: State to set the power policy to for all nodes. + :type state: 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_power_policy", state) + + def mc_reset(self, wait=False, async=False): + """Resets the management controller on all nodes. + + >>> fabric.mc_reset() + + :param wait: Wait for the nodes to come back up. + :type wait: boolean + + :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, "mc_reset", wait) + + def get_sensors(self, search="", async=False): + """Gets sensors from all nodes. + + >>> fabric.get_sensors() + { + 0: { + 'DRAM VDD Current' : , + 'DRAM VDD Voltage' : , + 'MP Temp 0' : , + 'Node Power' : , + 'TOP Temp 0' : , + 'TOP Temp 1' : , + 'TOP Temp 2' : , + 'Temp 0' : , + 'Temp 1' : , + 'Temp 2' : , + 'Temp 3' : , + 'V09 Current' : , + 'V09 Voltage' : , + 'V18 Current' : , + 'V18 Voltage' : , + 'VCORE Current' : , + 'VCORE Power' : , + 'VCORE Voltage' : + }, + # + # Output trimmed for brevity ... The output would be the same + # (format) for the remaining 3 ECMEs on this system. + # + }, + + .. note:: + * Output condensed for brevity. + * If the name parameter is not specified, all sensors are returned. + + :param name: Name of the sensor to get. (for all nodes) + :type name: string + :param async: Flag that determines if the command result (dictionary) + is returned or a Command object (can get status, etc.). + :type async: boolean + + """ + return self._run_on_all_nodes(async, "get_sensors", search) + + def get_firmware_info(self, async=False): + """Gets the firmware info from all nodes. + + >>> fabric.get_firmware_info() + { + 0: [, ...], + 1: [, ...], + 2: [, ...], + 3: [, ...] + } + + :param async: Flag that determines if the command result (dictionary) + is returned or a Command object (can get status, etc.). + :type async: boolean + + :return: THe firmware info for all nodes. + :rtype: dictionary or `Task `__ + + """ + return self._run_on_all_nodes(async, "get_firmware_info") + + def get_firmware_info_dict(self, async=False): + """Gets the firmware info from all nodes. + + >>> fabric.get_firmware_info_dict() + {0: + [ + # + # Each dictionary (in order) in this list represents the + # corresponding partition information + # + {# Partition 0 + 'daddr' : '20029000', + 'flags' : 'fffffffd', + 'in_use' : 'Unknown', + 'offset' : '00000000', + 'partition' : '00', + 'priority' : '0000000c', + 'size' : '00005000', + 'type' : '02 (S2_ELF)', + 'version' : 'v0.9.1' + }, + # Partitions 1 - 17 + ], + # + # Output trimmed for brevity ... The remaining Nodes in the Fabric + # would display all the partition format in the same manner. + # + } + + :param async: Flag that determines if the command result (dictionary) + is returned or a Command object (can get status, etc.). + :type async: boolean + + :return: The firmware info for all nodes. + :rtype: dictionary or `Task `__ + + """ + return self._run_on_all_nodes(async, "get_firmware_info_dict") + + def is_updatable(self, package, partition_arg="INACTIVE", priority=None, + async=False): + """Checks to see if all nodes can be updated with this fw package. + + >>> fabric.is_updatable(package=fwpkg) + {0: True, 1: True, 2: True, 3: True} + + :param package: Firmware package to test for updating. + :type package: `FirmwarePackage `_ + :param partition: Partition to test for updating. + :type partition: string + :param priority: SIMG Header priority. + :type priority: integer + :param async: Flag that determines if the command result (dictionary) + is returned or a Command object (can get status, etc.). + :type async: boolean + + :return: Whether or not a node can be updated with the specified + firmware package. + :rtype: dictionary or `Task `__ + + """ + return self._run_on_all_nodes(async, "is_updatable", package, + partition_arg, priority) + + def update_firmware(self, package, partition_arg="INACTIVE", + priority=None, async=False): + """Updates the firmware on all nodes. + + >>> fabric.update_firmware(package=fwpkg) + + :param package: Firmware package to update to. + :type package: `FirmwarePackage `_ + :param partition_arg: Which partition to update. + :type partition_arg: string + :param priority: SIMG header Priority setting. + :type priority: integer + :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, "update_firmware", package, + partition_arg, priority) + + def config_reset(self, async=False): + """Resets the configuration on all nodes to factory defaults. + + >>> fabric.config_reset() + {0: None, 1: None, 2: None, 3: None} + + :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, "config_reset") + + def set_boot_order(self, boot_args, async=False): + """Sets the boot order on all nodes. + + >>> fabric.set_boot_order(boot_args=['pxe', 'disk']) + + :param boot_args: Boot order list. + :type boot_args: list + :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_boot_order", boot_args) + + def get_boot_order(self, async=False): + """Gets the boot order from all nodes. + + >>> fabric.get_boot_order() + { + 0: ['disk', 'pxe'], + 1: ['disk', 'pxe'], + 2: ['disk', 'pxe'], + 3: ['disk', 'pxe'] + } + + :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 `__ + + """ + return self._run_on_all_nodes(async, "get_boot_order") + + def get_versions(self, async=False): + """Gets the version info from all nodes. + + >>> fabric.get_versions() + { + 0: , + 1: , + 2: , + 3: + } + + .. seealso:: + `Node.get_versions() `_ + + :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 basic SoC info for all nodes. + :rtype: dictionary or `Task `__ + + """ + return self._run_on_all_nodes(async, "get_versions") + + def get_versions_dict(self, async=False): + """Gets the version info from all nodes. + + >>> fabric.get_versions_dict() + {0: + { + 'a9boot_version' : 'v2012.10.16', + 'bootlog_version' : 'v0.9.1-39-g7e10987', + 'build_number' : '7E10987C', + 'card' : 'EnergyCard X02', + 'cdb_version' : 'v0.9.1-39-g7e10987', + 'dtb_version' : 'v3.6-rc1_cx_2012.10.02', + 'header' : 'Calxeda SoC (0x0096CD)', + 'soc_version' : 'v0.9.1', + 'stage2_version' : 'v0.9.1', + 'timestamp' : '1352911670', + 'uboot_version' : 'v2012.07_cx_2012.10.29', + 'ubootenv_version' : 'v2012.07_cx_2012.10.29', + 'version' : 'ECX-1000-v1.7.1' + }, + # + # Output trimmed for brevity ... Each remaining Nodes get_versions + # dictionary would be printed. + # + } + + .. seealso:: + `Node.get_versions_dict() `_ + + :param async: Flag that determines if the command result (dictionary) + is returned or a Task object (can get status, etc.). + :type async: boolean + + :returns: The basic SoC info for all nodes. + :rtype: dictionary or `Task `__ + + """ + return self._run_on_all_nodes(async, "get_versions_dict") + + def ipmitool_command(self, ipmitool_args, asynchronous=False): + """Run an arbitrary IPMItool command on all nodes. + + >>> # Gets eth0's MAC Address for each node ... + >>> fabric.ipmitool_command(['cxoem', 'fabric', 'get', 'macaddr', + >>> ...'interface', '0']) + { + 0: 'fc:2f:40:3b:ec:40', + 1: 'fc:2f:40:91:dc:40', + 2: 'fc:2f:40:ab:f7:14', + 3: 'fc:2f:40:88:b3:6c' + } + + :param ipmitool_args: Arguments to pass on to the ipmitool command. + :type ipmitool_args: list + :param async: Flag that determines if the command result (dictionary) + is returned or a Task object (can get status, etc.). + :type async: boolean + + :returns: IPMI command response. + :rtype: string + + """ + return self._run_on_all_nodes(asynchronous, "ipmitool_command", + ipmitool_args) + + def get_ubootenv(self, async=False): + """Gets the u-boot environment from all nodes. + + >>> fabric.get_ubootenv() + { + 0: , + 1: , + 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 + + :returns: UBootEnvironment objects for all nodes. + :rtype: dictionary or `Task `_ + + """ + return self._run_on_all_nodes(async, "get_ubootenv") + + def get_server_ip(self, interface=None, ipv6=False, user="user1", + password="1Password", aggressive=False, async=False): + """Get the server IP address from all nodes. The nodes must be powered + on for this to work. + + >>> fabric.get_server_ip() + { + 0: '192.168.100.100', + 1: '192.168.100.101', + 2: '192.168.100.102', + 3: '192.168.100.103' + } + + :param interface: Network interface to check (e.g. eth0). + :type interface: string + :param ipv6: Return an IPv6 address instead of IPv4. + :type ipv6: boolean + :param user: Linux username. + :type user: string + :param password: Linux password. + :type password: string + :param aggressive: Discover the IP aggressively (may power cycle node). + :type aggressive: boolean + :param async: Flag that determines if the command result (dictionary) + is returned or a Task object (can get status, etc.). + :type async: boolean + + :return: Server IP addresses for all nodes.. + :rtype: dictionary or `Task `_ + + """ + return self._run_on_all_nodes(async, "get_server_ip", interface, ipv6, + user, password, aggressive) + + def get_ipsrc(self): + """Return the ipsrc for the fabric. + + >>> fabric.get_ipsrc() + 2 + + :return: 1 for static, 2 for DHCP + :rtype: integer + """ + return self.primary_node.bmc.fabric_config_get_ip_src() + + def set_ipsrc(self, ipsrc_mode): + """Set the ipsrc for the fabric. + + >>> fabric.set_ipsrc(2) + + :param ipsrc_mode: 1 for static, 2 for DHCP + :type ipsrc_mode: integer + """ + self.primary_node.bmc.fabric_config_set_ip_src(ipsrc_mode) + + def apply_factory_default_config(self): + """Sets the fabric config to factory default + + >>> fabric.apply_factory_default_config() + """ + self.primary_node.bmc.fabric_config_factory_default() + + def get_ipaddr_base(self): + """The base IPv4 address for a range of static IP addresses used + for the nodes in the fabric + + >>> fabric.get_ipaddr_base() + '192.168.100.1' + + :return: The first IP address in the range of static IP addresses + :rtype: string + """ + return self.primary_node.bmc.fabric_config_get_ip_addr_base() + + def update_config(self): + """Push out updated configuration data for all nodes in the fabric. + + >>> fabric.update_config() + + """ + self.primary_node.bmc.fabric_config_update_config() + + def get_linkspeed(self): + """Get the global linkspeed for the fabric. In the partition world + this means the linkspeed for Configuration 0, Partition 0, Profile 0. + + >>> fabric.get_linkspeed() + 2.5 + + :return: Linkspeed for the fabric. + :rtype: float + + """ + return self.primary_node.bmc.fabric_config_get_linkspeed() + + def set_linkspeed(self, linkspeed): + """Set the global linkspeed for the fabric. In the partition world + this means the linkspeed for Configuration 0, Partition 0, Profile 0. + + >>> fabric.set_linkspeed(10) + + :param linkspeed: Linkspeed specified in Gbps. + :type linkspeed: float + + """ + self.primary_node.bmc.fabric_config_set_linkspeed(linkspeed) + + def add_macaddr(self, nodeid, iface, macaddr): + """Add a new macaddr to a node/interface in the fabric. + + >>> fabric.add_macaddr(3, 1, "66:55:44:33:22:11") + + :param nodeid: Node id to which the macaddr is to be added + :type nodeid: integer + :param iface: interface on the node to which the macaddr is to be added + :type iface: integer + :param macaddr: mac address to be added + :type macaddr: string + + """ + self.primary_node.bmc.fabric_add_macaddr(nodeid=nodeid, iface=iface, + macaddr=macaddr) + + def rm_macaddr(self, nodeid, iface, macaddr): + """Remove a macaddr to a node/interface in the fabric. + + >>> fabric.rm_macaddr(3, 1, "66:55:44:33:22:11") + + :param nodeid: Node id from which the macaddr is to be remove + :type nodeid: integer + :param iface: interface on the node from which the macaddr is to be removed + :type iface: integer + :param macaddr: mac address to be removed + :type macaddr: string + + """ + self.primary_node.bmc.fabric_rm_macaddr(nodeid=nodeid, iface=iface, + macaddr=macaddr) + + 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, + Profile 0. + + >>> fabric.get_linkspeed_policy() + 1 + + :return: Linkspeed Policy for the fabric. + :rtype: integer + + """ + return self.primary_node.bmc.fabric_config_get_linkspeed_policy() + + def set_linkspeed_policy(self, ls_policy): + """Set the global linkspeed policy for the fabric. In the partition + world this means the linkspeed policy for Configuration 0, + Partition 0, Profile 0. + + >>> fabric.set_linkspeed_policy(1) + + :param linkspeed: Linkspeed Policy. 0: Fixed, 1: Topological + :type linkspeed: integer + + """ + self.primary_node.bmc.fabric_config_set_linkspeed_policy(ls_policy) + + def get_link_users_factor(self): + """Get the global link users factor for the fabric. In the partition + world this means the link users factor for Configuration 0, + Partition 0, Profile 0. + + >>> fabric.get_link_users_factor() + 1 + + :return: Link users factor for the fabric. + :rtype: integer + + """ + return self.primary_node.bmc.fabric_config_get_link_users_factor() + + def set_link_users_factor(self, lu_factor): + """Set the global link users factor for the fabric. In the partition + world this means the link users factor for Configuration 0, + Partition 0, Profile 0. + + >>> fabric.set_link_users_factor(10) + + :param lu_factor: Multiplying factor for topological linkspeeds + :type lu_factor: integer + + """ + self.primary_node.bmc.fabric_config_set_link_users_factor(lu_factor) + + def get_uplink(self, iface=0): + """Get the uplink for an interface to xmit a packet out of the cluster. + + >>> fabric.get_uplink(0) + 0 + + :param iface: The interface for the uplink. + :type iface: integer + + :return: The uplink iface is using. + :rtype: integer + + """ + return self.primary_node.bmc.fabric_config_get_uplink(iface=iface) + + 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) + + :param uplink: The uplink to set. + :type uplink: integer + :param iface: The interface for the uplink. + :type iface: integer + + """ + self.primary_node.bmc.fabric_config_set_uplink(uplink=uplink, + iface=iface) + + def get_link_stats(self, link=0, async=False): + """Get the link_stats for each node in the fabric. + + :param link: The link to get stats for (0-4). + :type link: integer + + :param async: Flag that determines if the command result (dictionary) + is returned or a Task object (can get status, etc.). + :type async: boolean + + :returns: The link_stats for each link on each node. + :rtype: dictionary + + """ + return self._run_on_all_nodes(async, "get_link_stats", link) + + def get_linkmap(self, async=False): + """Get the linkmap for each node in the fabric. + + :param async: Flag that determines if the command result (dictionary) + is returned or a Task object (can get status, etc.). + :type async: boolean + + :returns: The linkmap for each node. + :rtype: dectionary + + """ + return self._run_on_all_nodes(async, "get_linkmap") + + def get_routing_table(self, async=False): + """Get the routing_table for the fabric. + + :param async: Flag that determines if the command result (dictionary) + is returned or a Task object (can get status, etc.). + :type async: boolean + + :returns: The routing_table for the fabric. + :rtype: dictionary + + """ + return self._run_on_all_nodes(async, "get_routing_table") + + def get_depth_chart(self, async=False): + """Get the depth_chart for the fabric. + + :param async: Flag that determines if the command result (dictionary) + is returned or a Task object (can get status, etc.). + :type async: boolean + + :returns: The depth_chart for the fabric. + :rtype: dictionary + + """ + return self._run_on_all_nodes(async, "get_depth_chart") + + def _run_on_all_nodes(self, async, name, *args): + """Start a command on all nodes.""" + tasks = {} + for node_id, node in self.nodes.iteritems(): + tasks[node_id] = self.task_queue.put(getattr(node, name), *args) + + if async: + return tasks + else: + results = {} + errors = {} + for node_id, task in tasks.iteritems(): + 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 + + def _discover_nodes(self, ip_address, username="admin", password="admin"): + """Gets the nodes of this fabric by pulling IP info from a BMC.""" + node = self.node(ip_address=ip_address, username=username, + password=password, tftp=self.tftp, + ecme_tftp_port=self.ecme_tftp_port, + verbose=self.verbose) + ipinfo = node.get_fabric_ipinfo() + for node_id, node_address in ipinfo.iteritems(): + self._nodes[node_id] = self.node(ip_address=node_address, + username=username, + password=password, + tftp=self.tftp, + ecme_tftp_port=self.ecme_tftp_port, + verbose=self.verbose) + self._nodes[node_id].node_id = node_id + + +# End of file: ./fabric.py diff --git a/cxmanage_api/firmware_package.py b/cxmanage_api/firmware_package.py new file mode 100644 index 0000000..433b596 --- /dev/null +++ b/cxmanage_api/firmware_package.py @@ -0,0 +1,168 @@ +# 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 os +import tarfile +import ConfigParser +import pkg_resources + +from cxmanage_api import temp_dir +from cxmanage_api.image import Image + + +class FirmwarePackage: + """A firmware update package contains multiple images & version information. + + .. note:: + * Valid firmware packages are in tar.gz format. + + >>> from cxmanage_api.firmware_package import FirmwarePackage + >>> fwpkg = FirmwarePackage('/path/to/ECX-1000_update-v1.7.1-dirty.tar.gz') + + :param filename: The file to extract and read. + :type filename: string + + :raises ValueError: If cxmanage version is too old. + + """ + + def __init__(self, filename=None): + """Default constructor for the FirmwarePackage class.""" + self.images = [] + self.version = None + self.config = None + self.required_socman_version = None + self.work_dir = temp_dir() + + if filename: + # Extract files and read config + try: + tarfile.open(filename, "r").extractall(self.work_dir) + except (IOError, tarfile.ReadError): + raise ValueError("%s is not a valid tar.gz file" + % os.path.basename(filename)) + config = ConfigParser.SafeConfigParser() + + if len(config.read(self.work_dir + "/MANIFEST")) == 0: + raise ValueError("%s is not a valid firmware package" + % os.path.basename(filename)) + + if "package" in config.sections(): + cxmanage_ver = config.get("package", + "required_cxmanage_version") + try: + pkg_resources.require("cxmanage>=%s" % cxmanage_ver) + except pkg_resources.VersionConflict: + # @todo: CxmanageVersionError? + raise ValueError( + "%s requires cxmanage version %s or later." + % (filename, cxmanage_ver)) + + if config.has_option("package", "required_socman_version"): + self.required_socman_version = config.get("package", + "required_socman_version") + if config.has_option("package", "firmware_version"): + self.version = config.get("package", "firmware_version") + if config.has_option("package", "firmware_config"): + self.config = config.get("package", "firmware_config") + + # Add all images from package + image_sections = [x for x in config.sections() if x != "package"] + for section in image_sections: + filename = "%s/%s" % (self.work_dir, section) + image_type = config.get(section, "type").upper() + simg = None + daddr = None + skip_crc32 = False + version = None + + # Read image options from config + if config.has_option(section, "simg"): + simg = config.getboolean(section, "simg") + if config.has_option(section, "daddr"): + daddr = int(config.get(section, "daddr"), 16) + if config.has_option(section, "skip_crc32"): + skip_crc32 = config.getboolean(section, "skip_crc32") + if config.has_option(section, "versionstr"): + version = config.get(section, "versionstr") + + self.images.append(Image(filename, image_type, simg, daddr, + skip_crc32, version)) + + def save_package(self, filename): + """Save all images as a firmware package. + + .. note:: + * Supports tar .gz and .bz2 file extensions. + + >>> from cxmanage_api.firmware_package import FirmwarePackage + >>> fwpkg = FirmwarePackage() + >>> fwpkg.save_package(filename='my_fw_update_pkg.tar.gz') + + :param filename: Name (or path) of of the file you wish to save. + :type filename: string + + """ + # Create the manifest + config = ConfigParser.SafeConfigParser() + for image in self.images: + section = os.path.basename(image.filename) + config.add_section(section) + config.set(section, "type", image.type) + config.set(section, "simg", str(image.simg)) + if image.priority != None: + config.set(section, "priority", str(image.priority)) + if image.daddr != None: + config.set(section, "daddr", "%x" % image.daddr) + if image.skip_crc32: + config.set(section, "skip_crc32", str(image.skip_crc32)) + if image.version != None: + config.set(section, "versionstr", image.version) + + manifest = open("%s/MANIFEST" % self.work_dir, "w") + config.write(manifest) + manifest.close() + + # Create the tar.gz package + if filename.endswith("gz"): + tar = tarfile.open(filename, "w:gz") + elif filename.endswith("bz2"): + tar = tarfile.open(filename, "w:bz2") + else: + tar = tarfile.open(filename, "w") + + tar.add("%s/MANIFEST" % self.work_dir, "MANIFEST") + for image in self.images: + tar.add(image.filename, os.path.basename(image.filename)) + tar.close() + + +# End of file: ./firmware_package.py diff --git a/cxmanage_api/image.py b/cxmanage_api/image.py new file mode 100644 index 0000000..23642c4 --- /dev/null +++ b/cxmanage_api/image.py @@ -0,0 +1,178 @@ +# 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 os +import subprocess + +from cxmanage_api import temp_file +from cxmanage_api.simg import create_simg, has_simg +from cxmanage_api.simg import valid_simg, get_simg_contents +from cxmanage_api.cx_exceptions import InvalidImageError + + +class Image: + """An Image consists of: an image type, a filename, and SIMG header info. + + >>> from cxmanage_api.image import Image + >>> img = Image(filename='spi_highbank.bin', image_type='PACAKGE') + + :param filename: Path to the image. + :type filename: string + :param image_type: Type of image. [CDB, BOOT_LOG, SOC_ELF] + :type image_type: string + :param simg: Path to the simg file. + :type simg: string + :param daddr: The daddr field in the SIMG Header. + :type daddr: integer + :param skip_crc32: Flag to skip (or not) CRC32 checking. + :type skip_crc32: boolean + :param version: Image version. + :type version: string + + :raises ValueError: If the image file does not exist. + :raises InvalidImageError: If the file is NOT a valid image. + + """ + + def __init__(self, filename, image_type, simg=None, daddr=None, + skip_crc32=False, version=None): + """Default constructor for the Image class.""" + self.filename = filename + self.type = image_type + self.daddr = daddr + self.skip_crc32 = skip_crc32 + self.version = version + + if (not os.path.exists(filename)): + raise ValueError("File %s does not exist" % filename) + + if (simg == None): + contents = open(filename).read() + self.simg = has_simg(contents) + else: + self.simg = simg + + if (not self.verify()): + raise InvalidImageError("%s is not a valid %s image" % + (filename, image_type)) + + def render_to_simg(self, priority, daddr): + """Creates a SIMG file. + + >>> img.render_to_simg(priority=1, daddr=0) + >>> 'spi_highbank.bin' + + :param priority: SIMG header priority value. + :type priority: integer + :param daddr: SIMG daddr field value. + :type daddr: integer + + :returns: The file name of the image. + :rtype: string + + :raises InvalidImageError: If the SIMG image is not valid. + + """ + filename = self.filename + # Create new image if necessary + if (not self.simg): + contents = open(filename).read() + # Figure out daddr + if (self.daddr != None): + daddr = self.daddr + # Create simg + align = (self.type in ["CDB", "BOOT_LOG"]) + simg = create_simg(contents, priority=priority, daddr=daddr, + skip_crc32=self.skip_crc32, align=align, + version=self.version) + filename = temp_file() + with open(filename, "w") as f: + f.write(simg) + + # Make sure the simg was built correctly + if (not valid_simg(open(filename).read())): + raise InvalidImageError("%s is not a valid SIMG" % + os.path.basename(self.filename)) + + return filename + + def size(self): + """Return the full size of this image (as an SIMG) + + >>> img.size() + 2174976 + + :returns: The size of the image file in bytes. + :rtype: integer + + """ + if (self.simg): + return os.path.getsize(self.filename) + else: + contents = open(self.filename).read() + align = (self.type in ["CDB", "BOOT_LOG"]) + simg = create_simg(contents, skip_crc32=True, align=align) + return len(simg) + + def verify(self): + """Returns true if the image is valid, false otherwise. + + >>> img.verify() + True + + :returns: Whether or not the image file is valid. + :rtype: boolean + + """ + if (self.type == "SOC_ELF"): + try: + file_process = subprocess.Popen(["file", self.filename], + stdout=subprocess.PIPE) + file_type = file_process.communicate()[0].split()[1] + + if (file_type != "ELF"): + return False + except OSError: + # "file" tool wasn't found, just continue without it + # typically located: /usr/bin/file + pass + + if (self.type in ["CDB", "BOOT_LOG"]): + # Look for "CDBH" + contents = open(self.filename).read() + if (self.simg): + contents = get_simg_contents(contents) + if (contents[:4] != "CDBH"): + return False + return True + + +# End of file: ./image.py diff --git a/cxmanage_api/ip_retriever.py b/cxmanage_api/ip_retriever.py new file mode 100644 index 0000000..411465b --- /dev/null +++ b/cxmanage_api/ip_retriever.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python + +# 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 re +import json + +import threading +from time import sleep + +from cxmanage_api.cx_exceptions import IPDiscoveryError + +from pexpect import TIMEOUT, EOF +from pyipmi import make_bmc +from pyipmi.server import Server +from pyipmi.bmc import LanBMC + + +class IPRetriever(threading.Thread): + """The IPRetriever class takes an ECME address and when run will + connect to the Linux Server from the ECME over SOL and use + ifconfig to determine the IP address. + """ + verbosity = None + aggressive = None + retry = None + timeout = None + interface = None + + ecme_ip = None + ecme_user = None + ecme_password = None + + server_ip = None + server_user = None + server_password = None + + def __init__(self, ecme_ip, aggressive=False, verbosity=0, **kwargs): + """Initializes the IPRetriever class. The IPRetriever needs the + only the first node to know where to start. + """ + super(IPRetriever, self).__init__() + self.daemon = True + + if hasattr(ecme_ip, 'ip_address'): + self.ecme_ip = ecme_ip.ip_address + else: + self.ecme_ip = ecme_ip + + self.aggressive = aggressive + self.verbosity = verbosity + + # Everything here is optional + self.timeout = kwargs.get('timeout', 120) + self.retry = kwargs.get('retry', 0) + + self.ecme_user = kwargs.get('ecme_user', 'admin') + self.ecme_password = kwargs.get('ecme_password', 'admin') + + self.server_user = kwargs.get('server_user', 'user1') + self.server_password = kwargs.get('server_password', '1Password') + + if '_inet_pattern' in kwargs and '_ip_pattern' in kwargs: + self.interface = kwargs.get('interface', None) + self._inet_pattern = kwargs['_inet_pattern'] + self._ip_pattern = kwargs['_ip_pattern'] + + else: + self.set_interface(kwargs.get('interface', None), + kwargs.get('ipv6', False)) + + if 'bmc' in kwargs: + self._bmc = kwargs['bmc'] + else: + self._bmc = make_bmc(LanBMC, verbose=(self.verbosity>1), + hostname=self.ecme_ip, + username=self.ecme_user, + password=self.ecme_password) + + if 'config_path' in kwargs: + self.read_config(kwargs['config_path']) + + + + def set_interface(self, interface=None, ipv6=False): + """Sets the interface and IP Version that is looked for on the server. + The interface must be acceptable by ifconfig. By default the first + interface given by ifconfig will be used. + """ + self.interface = interface + + if not ipv6: + self._ip_pattern = re.compile('\d+\.'*3 + '\d+') + self._inet_pattern = re.compile('inet addr:(%s)' % + self._ip_pattern.pattern) + else: + self._ip_pattern = re.compile('[0-9a-fA-F:]*:'*2 + '[0-9a-fA-F:]+') + self._inet_pattern = re.compile('inet6 addr: ?(%s)' % + self._ip_pattern.pattern) + + + def _log(self, msg, error=False): + """Print message with the ECME IP if verbosity is normal.""" + if error: + sys.stderr.write('Error %s: %s\n' % (self.ecme_ip, msg)) + elif self.verbosity > 0: + sys.stdout.write('%s: %s\n' % (self.ecme_ip, msg)) + + + def run(self): + """Attempts to finds the server IP address associated with the + ECME IP. If successful, server_ip will contain the IP address. + """ + if self.server_ip is not None: + self._log('Using stored IP %s' % self.server_ip) + return + + for attempt in range(self.retry + 1): + self.server_ip = self.sol_try_command(self.sol_find_ip) + + if self.server_ip is not None: + self._log('The server IP is %s' % self.server_ip) + return + + self._log('The server IP could not be found') + + + def _power_server(self, cycle=False): + """Puts the server in a powered state with conditions that should + result in a successful SOL activation. Returns True if successful. + """ + server = Server(self._bmc) + + if cycle: + self._log('Powering server off') + server.power_off() + sleep(5) + + if not server.is_powered: + self._log('Powering server on') + server.power_on() + sleep(10) + + return server.is_powered + + + def sol_find_ip(self, session): + """Uses ifconfig to get the IP address in an SOL session. + Returns the ip address if it is found or None on failure. + """ + if self.interface: + session.sendline('ifconfig %s' % self.interface) + else: + session.sendline('ifconfig') + + index = session.expect(['Link encap', 'error fetching interface', + TIMEOUT, EOF], timeout=2) + + # ifconfig found the interface + if index == 0: + output = ''.join(session.readline() for line in range(3)) + found_ip = self._inet_pattern.findall(output) + + if found_ip: + return found_ip[0] + else: + self._bmc.deactivate_payload() + raise IPDiscoveryError('Interface %s does not have ' + 'given address' % self.interface) + elif index == 1: + self._bmc.deactivate_payload() + raise IPDiscoveryError('Could not find interface %s' + % self.interface) + + else: # Failed to find interface. Returning None + return None + + + def sol_try_command(self, command): + """Connects to the server over a SOL connection. Attempts + to run the given command on the server without knowing + the state of the server. The command must return None if + it fails. If aggresive is True, then the server may be + restarted or power cycled to try and reset the state. + """ + server = Server(self._bmc) + if not server.is_powered: + self._log("Server is powered off. Can't proceed.") + raise IPDiscoveryError("Server is powered off. Can't proceed.") + + self._log('Activating SOL') + session = self._bmc.activate_payload() + sleep(2) + + timeout = self.timeout + attempt = 0 + login_attempted = False + + options = [TIMEOUT, EOF, + 'Highbank #', 'Invalid boot device', + '[lL]ogin:', '[pP]assword:', + 'network configuration', + 'going down for reboot', 'Stopped', + 'SOL payload already active', + 'SOL Session operational'] + + while attempt < 7: + index = session.expect(options, timeout) + + # Catchable errors + + # May need to boot + if index == 2: + session.sendline('run bootcmd_sata') + timeout = self.timeout + + # An invalid boot device can occur if bootcmd_sata fails + elif index == 3: + self._bmc.deactivate_payload() + raise IPDiscoveryError('Unable to boot linux due to ' + 'an invalid boot device') + + # Enter username or report incorrect login + elif index == 4: + if not login_attempted: + self._log('Logging into Linux') + session.sendline(self.server_user) + + # now check for failed login + options[index] = 'incorrect' + login_attempted = True + timeout = 4 + else: + self._bmc.deactivate_payload() + raise IPDiscoveryError('Incorrect username or password') + + # Enter password + elif index == 5: + session.sendline(self.server_password) + timeout = 4 + + # Warn about the network configuration + elif index == 6: + self._log('Waiting for network configuration') + timeout = self.timeout + + # Inform of reboot + elif index == 7: + self._log('Linux is rebooting') + timeout = self.timeout + + # Inform of zombied processes + elif index == 8: + self._log('Suspended the current process') + timeout = 2 + + # Try restarting SOL connection + elif index == 9: + self._log('Restarting SOL session') + self._bmc.deactivate_payload() + sleep(2) + session = self._bmc.activate_payload() + sleep(2) + session.sendline() + timeout = 8 + + # Successful SOL connection + elif index == 10: + self._log('SOL Activated') + session.sendline() + session.sendcontrol('z') + timeout = 2 + + else: + # Assume where are at a prompt and able to run the command + value = command(session) + + if value is not None: + self._bmc.deactivate_payload() + return value + + # Non catchable errors + + # Try to zombie the current process + if attempt == 0: + session.sendcontrol('z') + timeout = 2 + + elif not self.aggressive: + sleep(2) + self._bmc.deactivate_payload() + raise IPDiscoveryError('Unable to obtain the server\'s ' + 'IP address unintrusively') + + # Try sending kill signals + elif attempt == 1: + self._log('Sending interrupt signals') + session.sendcontrol('c') + timeout = 2 + + + elif attempt == 2: + session.sendcontrol('\\') + timeout = 2 + + # Try exiting. Will put us in login if we were another user + elif attempt == 3: + session.sendline('exit') + timeout = 4 + + # Attempt to reboot the Linux server + elif attempt == 4: + self._log('Attempting reboot') + session.sendline('sudo reboot') + sleep(1) + timeout = 4 + login_attempted = False + + # If all else fails: power cycle the server + elif attempt == 5: + self._power_server(cycle=True) + timeout = self.timeout + login_attempted = False + + attempt += 1 + + # Reaches here if nothing succeeds + self._bmc.deactivate_payload() + raise IPDiscoveryError('Unable to properly connect over SOL') + + + def read_config(self, path): + """Loads the address information from a json configuration + file written by write_config + """ + with open(path, 'r') as json_file: + json_data = json_file.read() + config_data = json.loads(json_data) + + self.ecme_ip = config_data['ecme_host'] + self.server_ip = config_data['server_host'] + + def write_config(self, path): + """Saves the address information in a json configuration file""" + config_data = {'ecme_host': self.ecme_ip, + 'server_host': self.server_ip} + + json_data = json.dumps(config_data, indent=4) + with open(path, 'w') as json_file: + json_file.write(json_data) + + + diff --git a/cxmanage_api/node.py b/cxmanage_api/node.py new file mode 100644 index 0000000..9ccae97 --- /dev/null +++ b/cxmanage_api/node.py @@ -0,0 +1,1507 @@ +# 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 os +import re +import subprocess +import time + +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 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 + + +class Node(object): + """A node is a single instance of an ECME. + + >>> # Typical usage ... + >>> from cxmanage_api.node import Node + >>> node = Node(ip_adress='10.20.1.9', verbose=True) + + :param ip_address: The ip_address of the Node. + :type ip_address: string + :param username: The login username credential. [Default admin] + :type username: string + :param password: The login password credential. [Default admin] + :type password: string + :param tftp: The internal/external TFTP server to use for data xfer. + :type tftp: `Tftp `_ + :param verbose: Flag to turn on verbose output (cmd/response). + :type verbose: boolean + :param bmc: BMC object for this Node. Default: pyipmi.bmc.LanBMC + :type bmc: BMC + :param image: Image object for this node. Default cxmanage_api.Image + :type image: `Image `_ + :param ubootenv: UbootEnv for this node. Default cxmanage_api.UbootEnv + :type ubootenv: `UbootEnv `_ + + """ + + def __init__(self, ip_address, username="admin", password="admin", + tftp=None, ecme_tftp_port=5001, verbose=False, bmc=None, + image=None, ubootenv=None, ipretriever=None): + """Default constructor for the Node class.""" + if (not tftp): + tftp = InternalTftp() + + # Dependency Integration + if (not bmc): + bmc = BMC + if (not image): + image = IMAGE + if (not ubootenv): + ubootenv = UBOOTENV + if (not ipretriever): + ipretriever = IPRETRIEVER + + self.ip_address = ip_address + self.username = username + self.password = password + self.tftp = tftp + self.ecme_tftp = ExternalTftp(ip_address, ecme_tftp_port) + self.verbose = verbose + + self.bmc = make_bmc(bmc, hostname=ip_address, username=username, + password=password, verbose=verbose) + self.image = image + self.ubootenv = ubootenv + self.ipretriever = ipretriever + + self._node_id = None + + def __eq__(self, other): + return isinstance(other, Node) and self.ip_address == other.ip_address + + def __hash__(self): + return hash(self.ip_address) + + def __str__(self): + return 'Node: %s' % self.ip_address + + @property + def tftp_address(self): + """Returns the tftp_address (ip:port) that this node is using. + + >>> node.tftp_address + '10.20.2.172:35123' + + :returns: The tftp address and port that this node is using. + :rtype: string + + """ + return '%s:%s' % (self.tftp.get_address(relative_host=self.ip_address), + self.tftp.port) + + @property + def node_id(self): + """ Returns the numerical ID for this node. + + >>> node.node_id + 0 + + :returns: The ID of this node. + :rtype: integer + + """ + if self._node_id == None: + self._node_id = self.bmc.fabric_get_node_id() + return self._node_id + + @node_id.setter + def node_id(self, value): + """ Sets the ID for this node. + + :param value: The value we want to set. + :type value: integer + + """ + self._node_id = value + + def get_mac_addresses(self): + """Gets a dictionary of MAC addresses for this node. The dictionary + maps each port/interface to a list of MAC addresses for that interface. + + >>> node.get_mac_addresses() + { + 0: ['fc:2f:40:3b:ec:40'], + 1: ['fc:2f:40:3b:ec:41'], + 2: ['fc:2f:40:3b:ec:42'] + } + + :return: MAC Addresses for all interfaces. + :rtype: dictionary + + """ + return self.get_fabric_macaddrs()[self.node_id] + + def add_macaddr(self, iface, macaddr): + """Add mac address on an interface + + >>> node.add_macaddr(iface, macaddr) + + :param iface: Interface to add to + :type iface: integer + :param macaddr: MAC address to add + :type macaddr: string + + :raises IpmiError: If errors in the command occur with BMC communication. + + """ + self.bmc.fabric_add_macaddr(iface=iface, macaddr=macaddr) + + def rm_macaddr(self, iface, macaddr): + """Remove mac address from an interface + + >>> node.rm_macaddr(iface, macaddr) + + :param iface: Interface to remove from + :type iface: integer + :param macaddr: MAC address to remove + :type macaddr: string + + :raises IpmiError: If errors in the command occur with BMC communication. + + """ + self.bmc.fabric_rm_macaddr(iface=iface, macaddr=macaddr) + + def get_power(self): + """Returns the power status for this node. + + >>> # Powered ON system ... + >>> node.get_power() + True + >>> # Powered OFF system ... + >>> node.get_power() + False + + :return: The power state of the Node. + :rtype: boolean + + """ + try: + return self.bmc.get_chassis_status().power_on + except IpmiError as e: + raise IpmiError(self._parse_ipmierror(e)) + + def set_power(self, mode): + """Send an IPMI power command to this target. + + >>> # To turn the power 'off' + >>> node.set_power(mode='off') + >>> # A quick 'get' to see if it took effect ... + >>> node.get_power() + False + + >>> # To turn the power 'on' + >>> node.set_power(mode='on') + + :param mode: Mode to set the power state to. ('on'/'off') + :type mode: string + + """ + try: + self.bmc.set_chassis_power(mode=mode) + except IpmiError as e: + raise IpmiError(self._parse_ipmierror(e)) + + def get_power_policy(self): + """Return power status reported by IPMI. + + >>> node.get_power_policy() + 'always-off' + + :return: The Nodes current power policy. + :rtype: string + + :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)) + + def set_power_policy(self, state): + """Set default power state for Linux side. + + >>> # Set the state to 'always-on' + >>> node.set_power_policy(state='always-on') + >>> # A quick check to make sure our setting took ... + >>> node.get_power_policy() + 'always-on' + + :param state: State to set the power policy to. + :type state: string + + """ + try: + self.bmc.set_chassis_policy(state) + except IpmiError as e: + raise IpmiError(self._parse_ipmierror(e)) + + def mc_reset(self, wait=False): + """Sends a Master Control reset command to the node. + + >>> node.mc_reset() + + :param wait: Wait for the node to come back up. + :type wait: boolean + + :raises Exception: If the BMC command contains errors. + :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)) + + if wait: + deadline = time.time() + 300.0 + + # Wait for it to go down... + time.sleep(60) + + # Now wait to come back up! + while time.time() < deadline: + time.sleep(1) + try: + self.bmc.get_info_basic() + break + except IpmiError: + pass + else: + raise Exception("Reset timed out") + + def get_sensors(self, search=""): + """Get a list of sensor objects that match search criteria. + + .. note:: + * If no sensor name is specified, ALL sensors will be returned. + + >>> # Get ALL sensors ... + >>> node.get_sensors() + { + 'MP Temp 0' : , + 'Temp 0' : , + 'Temp 1' : , + 'Temp 2' : , + 'Temp 3' : , + 'VCORE Voltage' : , + 'TOP Temp 2' : , + 'TOP Temp 1' : , + 'TOP Temp 0' : , + 'VCORE Current' : , + 'V18 Voltage' : , + 'V09 Current' : , + 'Node Power' : , + 'DRAM VDD Current' : , + 'DRAM VDD Voltage' : , + 'V18 Current' : , + 'VCORE Power' : , + 'V09 Voltage' : + } + >>> # Get ANY sensor that 'contains' the substring of search in it ... + >>> node.get_sensors(search='Temp 0') + { + 'MP Temp 0' : , + 'TOP Temp 0' : , + 'Temp 0' : + } + + :param search: Name of the sensor you wish to search for. + :type search: string + + :return: Sensor information. + :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)) + + if (len(sensors) == 0): + if (search == ""): + raise NoSensorError("No sensors were found") + else: + raise NoSensorError("No sensors containing \"%s\" were " + + "found" % search) + return dict((x.sensor_name, x) for x in sensors) + + def get_sensors_dict(self, search=""): + """Get a list of sensor dictionaries that match search criteria. + + >>> node.get_sensors_dict() + { + 'DRAM VDD Current': + { + 'entity_id' : '7.1', + 'event_message_control' : 'Per-threshold', + 'lower_critical' : '34.200', + 'lower_non_critical' : '34.200', + 'lower_non_recoverable' : '34.200', + 'maximum_sensor_range' : 'Unspecified', + 'minimum_sensor_range' : 'Unspecified', + 'negative_hysteresis' : '0.800', + 'nominal_reading' : '50.200', + 'normal_maximum' : '34.200', + 'normal_minimum' : '34.200', + 'positive_hysteresis' : '0.800', + 'sensor_name' : 'DRAM VDD Current', + 'sensor_reading' : '1.200 (+/- 0) Amps', + 'sensor_type' : 'Current', + 'status' : 'ok', + 'upper_critical' : '34.200', + 'upper_non_critical' : '34.200', + 'upper_non_recoverable' : '34.200' + }, + ... # + ... # Output trimmed for brevity ... many more sensors ... + ... # + 'VCORE Voltage': + { + 'entity_id' : '7.1', + 'event_message_control' : 'Per-threshold', + 'lower_critical' : '1.100', + 'lower_non_critical' : '1.100', + 'lower_non_recoverable' : '1.100', + 'maximum_sensor_range' : '0.245', + 'minimum_sensor_range' : 'Unspecified', + 'negative_hysteresis' : '0.020', + 'nominal_reading' : '1.000', + 'normal_maximum' : '1.410', + 'normal_minimum' : '0.720', + 'positive_hysteresis' : '0.020', + 'sensor_name' : 'VCORE Voltage', + 'sensor_reading' : '0 (+/- 0) Volts', + 'sensor_type' : 'Voltage', + 'status' : 'ok', + 'upper_critical' : '0.675', + 'upper_non_critical' : '0.695', + 'upper_non_recoverable' : '0.650' + } + } + >>> # Get ANY sensor name that has the string 'Temp 0' in it ... + >>> node.get_sensors_dict(search='Temp 0') + { + 'MP Temp 0': + { + 'entity_id' : '7.1', + 'event_message_control' : 'Per-threshold', + 'lower_critical' : '2.000', + 'lower_non_critical' : '5.000', + 'lower_non_recoverable' : '0.000', + 'maximum_sensor_range' : 'Unspecified', + 'minimum_sensor_range' : 'Unspecified', + 'negative_hysteresis' : '4.000', + 'nominal_reading' : '25.000', + 'positive_hysteresis' : '4.000', + 'sensor_name' : 'MP Temp 0', + 'sensor_reading' : '0 (+/- 0) degrees C', + 'sensor_type' : 'Temperature', + 'status' : 'ok', + 'upper_critical' : '70.000', + 'upper_non_critical' : '55.000', + 'upper_non_recoverable' : '75.000' + }, + 'TOP Temp 0': + { + 'entity_id' : '7.1', + 'event_message_control' : 'Per-threshold', + 'lower_critical' : '2.000', + 'lower_non_critical' : '5.000', + 'lower_non_recoverable' : '0.000', + 'maximum_sensor_range' : 'Unspecified', + 'minimum_sensor_range' : 'Unspecified', + 'negative_hysteresis' : '4.000', + 'nominal_reading' : '25.000', + 'positive_hysteresis' : '4.000', + 'sensor_name' : 'TOP Temp 0', + 'sensor_reading' : '33 (+/- 0) degrees C', + 'sensor_type' : 'Temperature', + 'status' : 'ok', + 'upper_critical' : '70.000', + 'upper_non_critical' : '55.000', + 'upper_non_recoverable' : '75.000' + }, + 'Temp 0': + { + 'entity_id' : '3.1', + 'event_message_control' : 'Per-threshold', + 'lower_critical' : '2.000', + 'lower_non_critical' : '5.000', + 'lower_non_recoverable' : '0.000', + 'maximum_sensor_range' : 'Unspecified', + 'minimum_sensor_range' : 'Unspecified', + 'negative_hysteresis' : '4.000', + 'nominal_reading' : '25.000', + 'positive_hysteresis' : '4.000', + 'sensor_name' : 'Temp 0', + 'sensor_reading' : '0 (+/- 0) degrees C', + 'sensor_type' : 'Temperature', + 'status' : 'ok', + 'upper_critical' : '70.000', + 'upper_non_critical' : '55.000', + 'upper_non_recoverable' : '75.000' + } + } + + .. note:: + * This function is the same as get_sensors(), only a dictionary of + **{sensor : {attributes :values}}** is returned instead of an + resultant pyipmi object. + + :param search: Name of the sensor you wish to search for. + :type search: string + + :return: Sensor information. + :rtype: dictionary of dictionaries + + """ + return dict((key, vars(value)) + for key, value in self.get_sensors(search=search).items()) + + def get_firmware_info(self): + """Gets firmware info for each partition on the Node. + + >>> node.get_firmware_info() + [, + , + , ...] + + :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)) + + def get_firmware_info_dict(self): + """Gets firmware info for each partition on the Node. + + .. note:: + * This function is the same as get_firmware_info(), only a + dictionary of **{attributes : values}** is returned instead of an + resultant FWInfo object. + + + >>> node.get_firmware_info_dict() + [ + {'daddr' : '20029000', + 'in_use' : 'Unknown', + 'partition' : '00', + 'priority' : '0000000c', + 'version' : 'v0.9.1', + 'flags' : 'fffffffd', + 'offset' : '00000000', + 'type' : '02 (S2_ELF)', + 'size' : '00005000'}, + .... # Output trimmed for brevity. + .... # partitions + .... # 1 - 16 + {'daddr' : '20029000', + 'in_use' : 'Unknown', + 'partition' : '17', + 'priority' : '0000000b', + 'version' : 'v0.9.1', + 'flags' : 'fffffffd', + 'offset' : '00005000', + 'type' : '02 (S2_ELF)', + 'size' : '00005000'} + ] + + :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. + + """ + return [vars(info) for info in self.get_firmware_info()] + + def is_updatable(self, package, partition_arg="INACTIVE", priority=None): + """Checks to see if the node can be updated with this firmware package. + + >>> from cxmanage_api.firmware_package import FirmwarePackage + >>> fwpkg = FirmwarePackage('ECX-1000_update-v1.7.1-dirty.tar.gz') + >>> fwpkg.version + 'ECX-1000-v1.7.1-dirty' + >>> node.is_updatable(fwpkg) + True + + :return: Whether the node is updatable or not. + :rtype: boolean + + """ + try: + self._check_firmware(package, partition_arg, priority) + return True + except (SocmanVersionError, FirmwareConfigError, PriorityIncrementError, + NoPartitionError, ImageSizeError, PartitionInUseError): + return False + + def update_firmware(self, package, partition_arg="INACTIVE", + priority=None): + """ Update firmware on this target. + + >>> from cxmanage_api.firmware_package import FirmwarePackage + >>> fwpkg = FirmwarePackage('ECX-1000_update-v1.7.1-dirty.tar.gz') + >>> fwpkg.version + 'ECX-1000-v1.7.1-dirty' + >>> node.update_firmware(package=fwpkg) + + :param package: Firmware package to deploy. + :type package: `FirmwarePackage `_ + :param partition_arg: Partition to upgrade to. + :type partition_arg: string + + :raises PriorityIncrementError: If the SIMG Header priority cannot be + changed. + + """ + fwinfo = self.get_firmware_info() + + # Get the new priority + if (priority == None): + priority = self._get_next_priority(fwinfo, package) + + updated_partitions = [] + + for image in package.images: + if (image.type == "UBOOTENV"): + # Get partitions + running_part = self._get_partition(fwinfo, image.type, "FIRST") + factory_part = self._get_partition(fwinfo, image.type, + "SECOND") + + # Update factory ubootenv + self._upload_image(image, factory_part, priority) + + # Update running ubootenv + old_ubootenv_image = self._download_image(running_part) + old_ubootenv = self.ubootenv(open( + old_ubootenv_image.filename).read()) + try: + ubootenv = self.ubootenv(open(image.filename).read()) + ubootenv.set_boot_order(old_ubootenv.get_boot_order()) + + 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, running_part, + priority) + except (ValueError, Exception): + self._upload_image(image, running_part, priority) + + updated_partitions += [running_part, factory_part] + else: + # Get the partitions + if (partition_arg == "BOTH"): + partitions = [self._get_partition(fwinfo, image.type, + "FIRST"), self._get_partition(fwinfo, image.type, + "SECOND")] + else: + partitions = [self._get_partition(fwinfo, image.type, + partition_arg)] + + # Update the image + for partition in partitions: + self._upload_image(image, partition, priority) + + updated_partitions += partitions + + if package.version: + self.bmc.set_firmware_version(package.version) + + # Post verify + fwinfo = self.get_firmware_info() + for old_partition in updated_partitions: + partition_id = int(old_partition.partition) + new_partition = fwinfo[partition_id] + + if new_partition.type != old_partition.type: + raise Exception("Update failed (partition %i, type changed)" + % partition_id) + + if int(new_partition.priority, 16) != priority: + raise Exception("Update failed (partition %i, wrong priority)" + % partition_id) + + if int(new_partition.flags, 16) & 2 != 0: + 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) + + + def config_reset(self): + """Resets configuration to factory defaults. + + >>> node.config_reset() + + :raises IpmiError: If errors in the command occur with BMC communication. + :raises Exception: If there are errors within the command response. + + """ + try: + # Reset CDB + result = self.bmc.reset_firmware() + if (hasattr(result, "error")): + raise Exception(result.error) + + # Reset ubootenv + fwinfo = self.get_firmware_info() + 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)) + + def set_boot_order(self, boot_args): + """Sets boot-able device order for this node. + + >>> node.set_boot_order(boot_args=['pxe', 'disk']) + + :param boot_args: Arguments list to pass on to the uboot environment. + :type boot_args: list + + """ + 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_boot_order(boot_args) + 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_boot_order(self): + """Returns the boot order for this node. + + >>> node.get_boot_order() + ['pxe', 'disk'] + + """ + return self.get_ubootenv().get_boot_order() + + def get_versions(self): + """Get version info from this node. + + >>> node.get_versions() + + >>> # Some useful information ... + >>> info.a9boot_version + 'v2012.10.16' + >>> info.cdb_version + 'v0.9.1' + + :returns: The results of IPMI info basic command. + :rtype: pyipmi.info.InfoBasicResult + + :raises IpmiError: If errors in the command occur with BMC communication. + :raises Exception: If there are errors within the command response. + + """ + try: + result = self.bmc.get_info_basic() + except IpmiError as e: + raise IpmiError(self._parse_ipmierror(e)) + + fwinfo = self.get_firmware_info() + components = [("cdb_version", "CDB"), + ("stage2_version", "S2_ELF"), + ("bootlog_version", "BOOT_LOG"), + ("a9boot_version", "A9_EXEC"), + ("uboot_version", "A9_UBOOT"), + ("ubootenv_version", "UBOOTENV"), + ("dtb_version", "DTB")] + for var, ptype in components: + try: + partition = self._get_partition(fwinfo, ptype, "ACTIVE") + setattr(result, var, partition.version) + except NoPartitionError: + pass + try: + card = self.bmc.get_info_card() + setattr(result, "hardware_version", "%s X%02i" % + (card.type, int(card.revision))) + except IpmiError as err: + if (self.verbose): + print str(err) + # Should raise an error, but we want to allow the command + # to continue gracefully if the ECME is out of date. + setattr(result, "hardware_version", "Unknown") + return result + + def get_versions_dict(self): + """Get version info from this node. + + .. note:: + * This function is the same as get_versions(), only a dictionary of + **{attributes : values}** is returned instead of an resultant + pyipmi object. + + >>> n.get_versions_dict() + {'soc_version' : 'v0.9.1', + 'build_number' : '7E10987C', + 'uboot_version' : 'v2012.07_cx_2012.10.29', + 'ubootenv_version' : 'v2012.07_cx_2012.10.29', + 'timestamp' : '1352911670', + 'cdb_version' : 'v0.9.1-39-g7e10987', + 'header' : 'Calxeda SoC (0x0096CD)', + 'version' : 'ECX-1000-v1.7.1', + 'bootlog_version' : 'v0.9.1-39-g7e10987', + 'a9boot_version' : 'v2012.10.16', + 'stage2_version' : 'v0.9.1', + 'dtb_version' : 'v3.6-rc1_cx_2012.10.02', + 'card' : 'EnergyCard X02' + } + + :returns: The results of IPMI info basic command. + :rtype: dictionary + + :raises IpmiError: If errors in the command occur with BMC communication. + :raises Exception: If there are errors within the command response. + + """ + return vars(self.get_versions()) + + def ipmitool_command(self, ipmitool_args): + """Send a raw ipmitool command to the node. + + >>> node.ipmitool_command(['cxoem', 'info', 'basic']) + 'Calxeda SoC (0x0096CD)\\n Firmware Version: ECX-1000-v1.7.1-dirty\\n + SoC Version: 0.9.1\\n Build Number: A69523DC \\n + Timestamp (1351543656): Mon Oct 29 15:47:36 2012' + + :param ipmitool_args: Arguments to pass to the ipmitool. + :type ipmitool_args: list + + """ + if ("IPMITOOL_PATH" in os.environ): + command = [os.environ["IPMITOOL_PATH"]] + else: + command = ["ipmitool"] + + command += ["-U", self.username, "-P", self.password, "-H", + self.ip_address] + command += ipmitool_args + + if (self.verbose): + print "Running %s" % " ".join(command) + + process = subprocess.Popen(command, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + return (stdout + stderr).strip() + + def get_ubootenv(self): + """Get the active u-boot environment. + + >>> node.get_ubootenv() + + + :return: U-Boot Environment object. + :rtype: `UBootEnv `_ + + """ + fwinfo = self.get_firmware_info() + partition = self._get_partition(fwinfo, "UBOOTENV", "ACTIVE") + image = self._download_image(partition) + return self.ubootenv(open(image.filename).read()) + + def get_fabric_ipinfo(self): + """Gets what ip information THIS node knows about the Fabric. + + >>> node.get_fabric_ipinfo() + {0: '10.20.1.9', 1: '10.20.2.131', 2: '10.20.0.220', 3: '10.20.2.5'} + + :return: Returns a map of node_ids->ip_addresses. + :rtype: dictionary + + :raises IpmiError: If the IPMI command fails. + :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)) + + # Parse addresses from ipinfo file + results = {} + for line in open(filename): + if (line.startswith("Node")): + elements = line.split() + node_id = int(elements[1].rstrip(":")) + node_ip_address = elements[2] + + # Old boards used to return 0.0.0.0 sometimes -- might not be + # an issue anymore. + if (node_ip_address != "0.0.0.0"): + results[node_id] = node_ip_address + + # Make sure we found something + if (not results): + raise TftpException("Node failed to reach TFTP server") + + return results + + def get_fabric_macaddrs(self): + """Gets what macaddr information THIS node knows about the Fabric. + + :return: Returns a map of node_ids->ports->mac_addresses. + :rtype: dictionary + + :raises IpmiError: If the IPMI command fails. + :raises TftpException: If the TFTP transfer fails. + + """ + try: + filename = self._run_fabric_command( + function_name='fabric_config_get_mac_addresses' + ) + + except IpmiError as e: + raise IpmiError(self._parse_ipmierror(e)) + + # Parse addresses from ipinfo file + results = {} + for line in open(filename): + if (line.startswith("Node")): + elements = line.split() + node_id = int(elements[1].rstrip(",")) + port = int(elements[3].rstrip(":")) + mac_address = elements[4] + + if not node_id in results: + results[node_id] = {} + if not port in results[node_id]: + results[node_id][port] = [] + results[node_id][port].append(mac_address) + + # Make sure we found something + if (not results): + raise TftpException("Node failed to reach TFTP server") + + return results + + def get_fabric_uplink_info(self): + """Gets what uplink information THIS node knows about the Fabric. + + >>> node.get_fabric_uplink_info() + {'0': {'eth0': '0', 'eth1': '0', 'mgmt': '0'}, + '1': {'eth0': '0', 'eth1': '0', 'mgmt': '0'}, + '2': {'eth0': '0', 'eth1': '0', 'mgmt': '0'}, + '3': {'eth0': '0', 'eth1': '0', 'mgmt': '0'}, + '4': {'eth0': '0', 'eth1': '0', 'mgmt': '0'}} + + :return: Returns a map of {node_id : {interface : uplink}} + :rtype: dictionary + + :raises IpmiError: If the IPMI command fails. + :raises TftpException: If the TFTP transfer fails. + + """ + filename = self._run_fabric_command( + function_name='fabric_config_get_uplink_info' + ) + + # Parse addresses from ipinfo file + results = {} + for line in open(filename): + node_id = int(line.replace('Node ', '')[0]) + ul_info = line.replace('Node %s:' % node_id, '').strip().split(',') + node_data = {} + for ul in ul_info: + data = tuple(ul.split()) + node_data[data[0]] = int(data[1]) + results[node_id] = node_data + + # Make sure we found something + if (not results): + raise TftpException("Node failed to reach TFTP server") + + return results + + def get_link_stats(self, link=0): + """Gets the linkstats for the link specified. + + :param link: The link to get stats for (0-4). + :type link: integer + + :returns: The linkstats for the link specified. + :rtype: dictionary + + :raises IpmiError: If the IPMI command fails. + + """ + filename = self._run_fabric_command( + function_name='fabric_get_linkstats', + link=link + ) + results = {} + for line in open(filename): + if ('=' in line): + reg_value = line.strip().split('=') + if (len(reg_value) < 2): + raise ValueError( + 'Register: %s has no value!' % reg_value[0] + ) + else: + results[ + reg_value[0].replace( + 'pFS_LCn', 'FS_LC%s' % link + ).replace('(link)', '').strip() + ] = reg_value[1].strip() + + # Make sure we found something + if (not results): + raise TftpException("Node failed to reach TFTP server") + + return results + + def get_linkmap(self): + """Gets the src and destination of each link on a node. + + :return: Returns a map of link_id->node_id. + :rtype: dictionary + + :raises IpmiError: If the IPMI command fails. + :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)) + + results = {} + for line in open(filename): + if (line.startswith("Link")): + elements = line.strip().split() + link_id = int(elements[1].rstrip(':')) + node_id = int(elements[3].strip()) + results[link_id] = node_id + + # Make sure we found something + if (not results): + raise TftpException("Node failed to reach TFTP server") + + return results + + def get_routing_table(self): + """Gets the routing table as instantiated in the fabric switch. + + :return: Returns a map of node_id->rt_entries. + :rtype: dictionary + + :raises IpmiError: If the IPMI command fails. + :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)) + + results = {} + for line in open(filename): + if (line.startswith("Node")): + elements = line.strip().split() + node_id = int(elements[1].rstrip(':')) + rt_entries = [] + for entry in elements[4].strip().split('.'): + rt_entries.append(int(entry)) + results[node_id] = rt_entries + + # Make sure we found something + if (not results): + raise TftpException("Node failed to reach TFTP server") + + return results + + def get_depth_chart(self): + """Gets a table indicating the distance from a given node to all other + nodes on each fabric link. + + :return: Returns a map of target->(neighbor, hops), + [other (neighbors,hops)] + :rtype: dictionary + + :raises IpmiError: If the IPMI command fails. + :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)) + + results = {} + for line in open(filename): + if (line.startswith("Node")): + elements = line.strip().split() + target = int(elements[1].rstrip(':')) + neighbor = int(elements[8].rstrip(':')) + hops = int(elements[4].strip()) + dchrt_entries = {} + dchrt_entries['shortest'] = (neighbor, hops) + try: + other_hops_neighbors = elements[12].strip().split('[,\s]+') + hops = [] + for entry in other_hops_neighbors: + pair = entry.strip().split('/') + hops.append((int(pair[1]), int(pair[0]))) + dchrt_entries['others'] = hops + except: + pass + + results[target] = dchrt_entries + + # Make sure we found something + if (not results): + raise TftpException("Node failed to reach TFTP server") + + return results + + def get_server_ip(self, interface=None, ipv6=False, user="user1", + password="1Password", aggressive=False): + """Get the IP address of the Linux server. The server must be powered + on for this to work. + + >>> node.get_server_ip() + '192.168.100.100' + + :param interface: Network interface to check (e.g. eth0). + :type interface: string + :param ipv6: Return an IPv6 address instead of IPv4. + :type ipv6: boolean + :param user: Linux username. + :type user: string + :param password: Linux password. + :type password: string + :param aggressive: Discover the IP aggressively (may power cycle node). + :type aggressive: boolean + + :return: The IP address of the server. + :rtype: string + :raises IpmiError: If errors in the command occur with BMC communication. + :raises IPDiscoveryError: If the server is off, or the IP can't be obtained. + + """ + verbosity = 2 if self.verbose else 0 + retriever = self.ipretriever(self.ip_address, aggressive=aggressive, + verbosity=verbosity, server_user=user, server_password=password, + interface=interface, ipv6=ipv6, bmc=self.bmc) + retriever.run() + return retriever.server_ip + + def get_linkspeed(self, link=None, actual=False): + """Get the linkspeed for the node. This returns either + the actual linkspeed based on phy controller register settings, + or if sent to a primary node, the linkspeed setting for the + Profile 0 of the currently active Configuration. + + >>> fabric.get_linkspeed() + 2.5 + + :param link: The fabric link number to read the linkspeed for. + :type link: integer + :param actual: WhetherThe fabric link number to read the linkspeed for. + :type actual: boolean + + :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)) + + 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) + 0 + + :param iface: The interface for the uplink. + :type iface: integer + + :return: The uplink iface is connected to. + :rtype: integer + + :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)) + + def set_uplink(self, uplink=0, iface=0): + """Set the uplink a MAC will use when transmitting a packet out of the + cluster. + + >>> # + >>> # Set eth0 to uplink 1 ... + >>> # + >>> fabric.set_uplink(uplink=1,iface=0) + + :param uplink: The uplink to set. + :type uplink: integer + :param iface: The interface for the uplink. + :type iface: integer + + :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)) + + def _run_fabric_command(self, function_name, **kwargs): + """Handles the basics of sending a node a command for fabric data.""" + filename = temp_file() + basename = os.path.basename(filename) + try: + getattr(self.bmc, function_name)(filename=basename, **kwargs) + self.ecme_tftp.get_file(basename, filename) + + except (IpmiError, TftpException) as e: + try: + getattr(self.bmc, function_name)( + filename=basename, + tftp_addr=self.tftp_address, + **kwargs + ) + + except IpmiError as e: + raise IpmiError(self._parse_ipmierror(e)) + + deadline = time.time() + 10 + while (time.time() < deadline): + try: + time.sleep(1) + self.tftp.get_file(src=basename, dest=filename) + if (os.path.getsize(filename) > 0): + break + + except (TftpException, IOError): + pass + + return filename + + def _get_partition(self, fwinfo, image_type, partition_arg): + """Get a partition for this image type based on the argument.""" + # Filter partitions for this type + partitions = [x for x in fwinfo if + x.type.split()[1][1:-1] == image_type] + if (len(partitions) < 1): + raise NoPartitionError("No partition of type %s found on host" + % image_type) + + if (partition_arg == "FIRST"): + return partitions[0] + elif (partition_arg == "SECOND"): + if (len(partitions) < 2): + raise NoPartitionError("No second partition found on host") + return partitions[1] + elif (partition_arg == "OLDEST"): + # Return the oldest partition + partitions.sort(key=lambda x: x.partition, reverse=True) + partitions.sort(key=lambda x: x.priority) + return partitions[0] + elif (partition_arg == "NEWEST"): + # Return the newest partition + partitions.sort(key=lambda x: x.partition) + partitions.sort(key=lambda x: x.priority, reverse=True) + return partitions[0] + elif (partition_arg == "INACTIVE"): + # Return the partition that's not in use (or least likely to be) + partitions.sort(key=lambda x: x.partition, reverse=True) + partitions.sort(key=lambda x: x.priority) + partitions.sort(key=lambda x: int(x.flags, 16) & 2 == 0) + partitions.sort(key=lambda x: x.in_use == "1") + return partitions[0] + elif (partition_arg == "ACTIVE"): + # Return the partition that's in use (or most likely to be) + partitions.sort(key=lambda x: x.partition) + partitions.sort(key=lambda x: x.priority, reverse=True) + partitions.sort(key=lambda x: int(x.flags, 16) & 2 == 1) + partitions.sort(key=lambda x: x.in_use == "0") + return partitions[0] + else: + raise ValueError("Invalid partition argument: %s" % partition_arg) + + def _upload_image(self, image, partition, priority=None): + """Upload a single image. This includes uploading the image, performing + the firmware update, crc32 check, and activation. + """ + partition_id = int(partition.partition) + if (priority == None): + priority = int(partition.priority, 16) + daddr = int(partition.daddr, 16) + + # Check image size + if (image.size() > int(partition.size, 16)): + raise ImageSizeError("%s image is too large for partition %i" % + (image.type, partition_id)) + + filename = image.render_to_simg(priority, daddr) + basename = os.path.basename(filename) + + try: + self.bmc.register_firmware_write(basename, partition_id, image.type) + self.ecme_tftp.put_file(filename, basename) + except (IpmiError, TftpException): + # Fall back and use TFTP server + self.tftp.put_file(filename, basename) + result = self.bmc.update_firmware(basename, partition_id, + image.type, self.tftp_address) + if (not hasattr(result, "tftp_handle_id")): + raise AttributeError("Failed to start firmware upload") + self._wait_for_transfer(result.tftp_handle_id) + + # Verify crc and activate + result = self.bmc.check_firmware(partition_id) + if ((not hasattr(result, "crc32")) or (result.error != None)): + raise AttributeError("Node reported crc32 check failure") + self.bmc.activate_firmware(partition_id) + + def _download_image(self, partition): + """Download an image from the target.""" + filename = temp_file() + basename = os.path.basename(filename) + partition_id = int(partition.partition) + image_type = partition.type.split()[1][1:-1] + + try: + self.bmc.register_firmware_read(basename, partition_id, image_type) + self.ecme_tftp.get_file(basename, filename) + except (IpmiError, TftpException): + # 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) + + return self.image(filename=filename, image_type=image_type, + daddr=int(partition.daddr, 16), + version=partition.version) + + def _wait_for_transfer(self, handle): + """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") + + def _check_firmware(self, package, partition_arg="INACTIVE", priority=None): + """Check if this host is ready for an update.""" + info = self.get_versions() + fwinfo = self.get_firmware_info() + + # Check firmware version + if package.version and info.firmware_version: + package_match = re.match("^ECX-[0-9]+", package.version) + firmware_match = re.match("^ECX-[0-9]+", info.firmware_version) + if package_match and firmware_match: + package_version = package_match.group(0) + firmware_version = firmware_match.group(0) + if package_version != firmware_version: + raise FirmwareConfigError( + "Refusing to upload an %s package to an %s host" + % (package_version, firmware_version)) + + # Check socman version + if (package.required_socman_version): + ecme_version = info.ecme_version.lstrip("v") + required_version = package.required_socman_version.lstrip("v") + if ((package.required_socman_version and + parse_version(ecme_version)) < + parse_version(required_version)): + raise SocmanVersionError( + "Update requires socman version %s (found %s)" + % (required_version, ecme_version)) + + # Check slot0 vs. slot2 + # TODO: remove this check + if (package.config and info.firmware_version != "Unknown" and + len(info.firmware_version) < 32): + if "slot2" in info.firmware_version: + firmware_config = "slot2" + else: + firmware_config = "default" + + if (package.config != firmware_config): + raise FirmwareConfigError( + "Refusing to upload a \'%s\' package to a \'%s\' host" + % (package.config, firmware_config)) + + # Check that the priority can be bumped + if (priority == None): + priority = self._get_next_priority(fwinfo, package) + + # Check partitions + for image in package.images: + if ((image.type == "UBOOTENV") or (partition_arg == "BOTH")): + partitions = [self._get_partition(fwinfo, image.type, x) + for x in ["FIRST", "SECOND"]] + else: + partitions = [self._get_partition(fwinfo, image.type, + partition_arg)] + + for partition in partitions: + if (image.size() > int(partition.size, 16)): + raise ImageSizeError( + "%s image is too large for partition %i" + % (image.type, int(partition.partition))) + + if (image.type in ["CDB", "BOOT_LOG"] and + partition.in_use == "1"): + raise PartitionInUseError( + "Can't upload to a CDB/BOOT_LOG partition that's in use") + + return True + + def _get_next_priority(self, fwinfo, package): + """ Get the next priority """ + priority = None + image_types = [x.type for x in package.images] + for partition in fwinfo: + partition_active = int(partition.flags, 16) & 2 + partition_type = partition.type.split()[1].strip("()") + if ((not partition_active) and (partition_type in image_types)): + priority = max(priority, int(partition.priority, 16) + 1) + if (priority > 0xFFFF): + raise PriorityIncrementError( + "Unable to increment SIMG priority, too high") + return priority + + def _parse_ipmierror(self, error_details): + """Parse a meaningful message from an IpmiError """ + try: + error = str(error_details).lstrip().splitlines()[0].rstrip() + if (error.startswith('Error: ')): + error = error[7:] + return error + except IndexError: + return 'Unknown IPMItool error.' + + +# End of file: ./node.py diff --git a/cxmanage_api/simg.py b/cxmanage_api/simg.py new file mode 100644 index 0000000..6ae8bf8 --- /dev/null +++ b/cxmanage_api/simg.py @@ -0,0 +1,239 @@ +# 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 struct + +from cxmanage_api.crc32 import get_crc32 + + +HEADER_LENGTH = 60 +MIN_HEADER_LENGTH = 28 + + +class SIMGHeader: + """Container for an SIMG header. + + >>> from cxmanage_api.simg import SIMGHeader + >>> simg = SIMGHeader() + + :param header_string: SIMG Header value. + :type header_string: string + + """ + + def __init__(self, header_string=None): + """Default constructor for the SIMGHeader class.""" + if (header_string == None): + self.magic_string = 'SIMG' + self.hdrfmt = 2 + self.priority = 0 + self.imgoff = HEADER_LENGTH + self.imglen = 0 + self.daddr = 0 + self.flags = 0 + self.crc32 = 0 + self.version = '' + else: + header_string = header_string.ljust(HEADER_LENGTH, chr(0)) + tup = struct.unpack('<4sHHIIIII32s', header_string) + self.magic_string = tup[0] + self.hdrfmt = tup[1] + self.priority = tup[2] + self.imgoff = tup[3] + self.imglen = tup[4] + self.daddr = tup[5] + self.flags = tup[6] + self.crc32 = tup[7] + if (self.hdrfmt >= 2): + self.version = tup[8] + else: + self.version = '' + + def __str__(self): + return struct.pack('<4sHHIIIII32s', self.magic_string, self.hdrfmt, + self.priority, self.imgoff, self.imglen, self.daddr, + self.flags, self.crc32, self.version) + +def create_simg(contents, priority=0, daddr=0, skip_crc32=False, align=False, + version=None): + """Create an SIMG version of a file. + + >>> from cxmanage_api.simg import create_simg + >>> simg = create_simg(contents='foobarbaz') + >>> simg + 'SIMG\\x02\\x00\\x00\\x00<\\x00\\x00\\x00\\t\\x00\\x00\\x00\\x00\\x00\\x00 + \\x00\\xff\\xff\\xff\\xffK\\xf3\\xea\\x0c\\x00\\x00\\x00\\x00\\x00\\x00 + \\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00 + \\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00foobarbaz' + + :param contents: Contents of the SIMG file. + :type contents: string + :param priority: SIMG Header priority value. + :type priority: integer + :param daddr: SIMG Header daddr value. + :type daddr: integer + :param skip_crc32: Flag to skip crc32 calculating. + :type skip_crc32: boolean + :param align: Flag used to turn on/off image offset of 4096. + :type align: boolean + :param version: Version string. + :type version: string + + :returns: String representation of the SIMG file. + :rtype: string + + """ + if (version == None): + version = '' + + header = SIMGHeader() + header.priority = priority + header.imglen = len(contents) + header.daddr = daddr + header.version = version + + if (align): + header.imgoff = 4096 + # Calculate crc value + if (skip_crc32): + crc32 = 0 + else: + crc32 = get_crc32(contents, get_crc32(str(header)[:MIN_HEADER_LENGTH])) + # Get SIMG header + header.flags = 0xFFFFFFFF + header.crc32 = crc32 + return str(header).ljust(header.imgoff, chr(0)) + contents + +def has_simg(simg): + """Returns true if this string has an SIMG header. + + >>> from cxmanage_api.simg import create_simg + >>> simg=create_simg(contents='foobarbaz') + >>> from cxmanage_api.simg import has_simg + >>> has_simg(simg=simg) + True + + :param simg: SIMG string (representation of a SIMG file). + :type simg: string + + :returns: Whether or not the string has a SIMG header. + :rtype: boolean + + """ + if (len(simg) < MIN_HEADER_LENGTH): + return False + header = SIMGHeader(simg[:HEADER_LENGTH]) + # Check for magic word + return (header.magic_string == 'SIMG') + +def valid_simg(simg): + """Return true if this is a valid SIMG. + + >>> from cxmanage_api.simg import create_simg + >>> simg=create_simg(contents='foobarbaz') + >>> from cxmanage_api.simg import valid_simg + >>> valid_simg(simg=simg) + True + + :param simg: SIMG string (representation of a SIMG file). + :type simg: string + + :returns: Whether or not the SIMG is valid. + :rtype: boolean + + """ + if (not has_simg(simg)): + return False + header = SIMGHeader(simg[:HEADER_LENGTH]) + + # Check offset + if (header.imgoff < MIN_HEADER_LENGTH): + return False + + # Check length + start = header.imgoff + end = start + header.imglen + contents = simg[start:end] + if (len(contents) < header.imglen): + return False + + # Check crc32 + crc32 = header.crc32 + if (crc32 != 0): + header.flags = 0 + header.crc32 = 0 + if (crc32 != get_crc32(contents, + get_crc32(str(header)[:MIN_HEADER_LENGTH]))): + return False + return True + +def get_simg_header(simg): + """Returns the header of this SIMG. + + >>> from cxmanage_api.simg import get_simg_header + >>> get_simg_header(x) + + + :param simg: Path to SIMG file. + :type simg: string + + :returns: The SIMG header. + :rtype: string + + :raises ValueError: If the SIMG cannot be read. + + """ + if (not valid_simg(simg)): + raise ValueError("Failed to read invalid SIMG") + return SIMGHeader(simg[:HEADER_LENGTH]) + +def get_simg_contents(simg): + """Returns the contents of this SIMG. + + >>> from cxmanage_api.simg import get_simg_contents + >>> get_simg_contents(simg=simg) + 'foobarbaz' + + :param simg: Path to SIMG file. + :type simg: string + + :returns: Contents of this SIMG. + :rtype: string + + """ + header = get_simg_header(simg) + start = header.imgoff + end = start + header.imglen + return simg[start:end] + + +# End of file: ./simg.py + diff --git a/cxmanage_api/tasks.py b/cxmanage_api/tasks.py new file mode 100644 index 0000000..6b5cfde --- /dev/null +++ b/cxmanage_api/tasks.py @@ -0,0 +1,175 @@ +# 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 collections import deque +from threading import Thread, Lock, Event +from time import sleep + + +class Task(object): + """A task object represents some unit of work to be done. + + :param method: The actual method (function) to execute. + :type method: function + :param args: Arguments to pass to the named method to run. + :type args: list + """ + + def __init__(self, method, *args): + """Default constructor for the Task class.""" + self.status = "Queued" + self.result = None + self.error = None + + self._method = method + self._args = args + self._finished = Event() + + def join(self): + """Wait for this task to finish.""" + self._finished.wait() + + def is_alive(self): + """Return true if this task hasn't been finished. + + :returns: Whether or not the task is still alive. + :rtype: boolean + + """ + return not self._finished.is_set() + + def _run(self): + """Execute this task. Should only be called by TaskWorker.""" + self.status = "In Progress" + try: + self.result = self._method(*self._args) + self.status = "Completed" + except Exception as e: + self.error = e + self.status = "Failed" + + self._finished.set() + + +class TaskQueue(object): + """A task queue, consisting of a queue and a number of workers. + + :param threads: Number of threads to create (if needed). + :type threads: integer + :param delay: Time to wait between + """ + + def __init__(self, threads=48, delay=0): + """Default constructor for the TaskQueue class.""" + self.threads = threads + self.delay = delay + + self._lock = Lock() + self._queue = deque() + self._workers = 0 + + def put(self, method, *args): + """Add a task to the task queue, and spawn a worker if we're not full. + + :param method: Named method to run. + :type method: string + :param args: Arguments to pass to the named method to run. + :type args: list + + :returns: A Task that will be executed by a worker at a later time. + :rtype: Task + + """ + self._lock.acquire() + + task = Task(method, *args) + self._queue.append(task) + + if self._workers < self.threads: + TaskWorker(task_queue=self, delay=self.delay) + self._workers += 1 + + self._lock.release() + return task + + def get(self): + """ + Get a task from the task queue. Mainly used by workers. + + :returns: A Task object that hasn't been executed yet. + :rtype: Task + + :raises IndexError: If there are no tasks in the queue. + + """ + self._lock.acquire() + try: + return self._queue.popleft() + finally: + self._lock.release() + + def _remove_worker(self): + """Decrement the worker count. Should only be used by TaskWorker.""" + self._lock.acquire() + self._workers -= 1 + self._lock.release() + + +class TaskWorker(Thread): + """A worker thread that runs tasks from a TaskQueue. + + :param task_queue: Task queue to get tasks from. + :type task_queue: TaskQueue + :param delay: Time to wait in-between execution. + + """ + def __init__(self, task_queue, delay=0): + super(TaskWorker, self).__init__() + self.daemon = True + + self._task_queue = task_queue + self._delay = delay + + self.start() + + def run(self): + """Repeatedly get tasks from the TaskQueue and execute them.""" + try: + while True: + sleep(self._delay) + task = self._task_queue.get() + task._run() + except: + self._task_queue._remove_worker() + +DEFAULT_TASK_QUEUE = TaskQueue() + +# End of file: ./tasks.py diff --git a/cxmanage_api/tftp.py b/cxmanage_api/tftp.py new file mode 100644 index 0000000..02b7c49 --- /dev/null +++ b/cxmanage_api/tftp.py @@ -0,0 +1,297 @@ +# 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 os +import sys +import atexit +import shutil +import socket +import logging +import traceback + +from tftpy import TftpClient, TftpServer, setLogLevel +from threading import Thread +from cxmanage_api import temp_dir +from tftpy.TftpShared import TftpException + + +class InternalTftp(object): + """Internally serves files using the `Trivial File Transfer Protocol `_. + + >>> # Typical instantiation ... + >>> from cxmanage_api.tftp import InternalTftp + >>> i_tftp = InternalTftp() + >>> # Alternatively, you can specify an address or hostname ... + >>> i_tftp = InternalTftp(ip_address='localhost') + + :param ip_address: Ip address for the Internal TFTP server to use. + :type ip_address: string + :param port: Port for the internal TFTP server. + :type port: integer + :param verbose: Flag to turn on additional messaging. + :type verbose: boolean + + """ + + def __init__(self, ip_address=None, port=0, verbose=False): + """Default constructor for the InternalTftp class.""" + self.tftp_dir = temp_dir() + self.verbose = verbose + pipe = os.pipe() + pid = os.fork() + if (not pid): + # Force tftpy to use sys.stdout and sys.stderr + try: + os.dup2(sys.stdout.fileno(), 1) + os.dup2(sys.stderr.fileno(), 2) + + except AttributeError, err_msg: + if (self.verbose): + print ('Passing on exception: %s' % err_msg) + pass + + # Create a PortThread class only if needed ... + class PortThread(Thread): + """Thread that sends the port number through the pipe.""" + def run(self): + """Run function override.""" + # Need to wait for the server to open its socket + while not server.sock: + pass + with os.fdopen(pipe[1], "w") as a_file: + a_file.write("%i\n" % server.sock.getsockname()[1]) + # + # Create an Internal TFTP server thread + # + server = TftpServer(tftproot=self.tftp_dir) + thread = PortThread() + thread.start() + try: + if not self.verbose: + setLogLevel(logging.CRITICAL) + # Start accepting connections ... + server.listen(listenport=port) + except KeyboardInterrupt: + # User @ keyboard cancelled server ... + if (self.verbose): + traceback.format_exc() + sys.exit(0) + + self.server = pid + self.ip_address = ip_address + with os.fdopen(pipe[0]) as a_fd: + self.port = int(a_fd.readline()) + atexit.register(self.kill) + + def get_address(self, relative_host=None): + """Returns the ipv4 address of this server. + If a relative_host is specified, then we discover our address to them. + + >>> i_tftp.get_address(relative_host='10.10.14.150') + 'localhost' + + :param relative_host: Ip address to the relative host. + :type relative_host: string + + :return: The ipv4 address of this InternalTftpServer. + :rtype: string + + """ + if (self.ip_address != None): + return self.ip_address + elif (relative_host == None): + return "localhost" + else: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.connect((relative_host, self.port)) + ipv4 = sock.getsockname()[0] + sock.close() + return ipv4 + + def kill(self): + """Kills the InternalTftpServer. + + >>> i_tftp.kill() + + """ + if (self.server): + os.kill(self.server, 15) + self.server = None + + def get_file(self, src, dest): + """Download a file from the tftp server to local_path. + + >>> i_tftp.get_file(src='remote_file_i_want.txt', dest='/local/path') + + :param src: Source file path on the tftp_server. + :type src: string + :param dest: Destination path (on your machine) to copy the TFTP file to. + :type dest: string + + """ + src = "%s/%s" % (self.tftp_dir, src) + if (src != dest): + try: + # Ensure the file exists ... + with open(src) as a_file: + a_file.close() + shutil.copy(src, dest) + + except Exception: + traceback.format_exc() + raise + + def put_file(self, src, dest): + """Upload a file from src to dest on the tftp server (path). + + >>> i_tftp.put_file(src='/local/file.txt', dest='remote_file_name.txt') + + :param src: Path to the local file to send to the TFTP server. + :type src: string + :param dest: Path to put the file to on the TFTP Server. + :type dest: string + + """ + dest = "%s/%s" % (self.tftp_dir, dest) + if (src != dest): + try: + # Ensure that the local file exists ... + with open(src) as a_file: + a_file.close() + shutil.copy(src, dest) + except Exception: + traceback.format_exc() + raise + + +class ExternalTftp(object): + """Defines a ExternalTftp object, which is actually TFTP client. + + >>> from cxmanage_api.tftp import ExternalTftp + >>> e_tftp = ExternalTftp(ip_address='1.2.3.4') + + :param ip_address: Ip address of the TFTP server. + :type ip_address: string + :param port: Port to the External TFTP server. + :type port: integer + :param verbose: Flag to turn on verbose output (cmd/response). + :type verbose: boolean + + """ + + def __init__(self, ip_address, port=69, verbose=False): + """Default constructor for this the ExternalTftp class.""" + self.ip_address = ip_address + self.port = port + self.verbose = verbose + + if not self.verbose: + setLogLevel(logging.CRITICAL) + + def get_address(self, relative_host=None): + """Return the ip address of the ExternalTftp server. + + >>> e_tftp.get_address() + '1.2.3.4' + + :param relative_host: Unused parameter present only for function signature. + :type relative_host: None + + :returns: The ip address of the external TFTP server. + :rtype: string + + """ + del relative_host # Needed only for function signature. + return self.ip_address + + def get_file(self, src, dest): + """Download a file from the ExternalTftp Server. + + .. note:: + * TftpClient is not threadsafe, so we create a unique instance for + each transfer. + + >>> e_tftp.get_file(src='remote_file_i_want.txt', dest='/local/path') + + :param src: The path to the file on the Tftp server. + :type src: string + :param dest: The local destination to copy the file to. + :type dest: string + + :raises TftpException: If the file does not exist or cannot be obtained + from the TFTP server. + :raises TftpException: If a TypeError is received from tftpy. + + """ + try: + client = TftpClient(self.ip_address, self.port) + client.download(output=dest, filename=src) + except TftpException: + if (self.verbose): + traceback.format_exc() + raise + except TypeError: + if (self.verbose): + traceback.format_exc() + raise TftpException("Failed download file from TFTP server") + + def put_file(self, src, dest): + """Uploads a file to the tftp server. + + .. note:: + * TftpClient is not threadsafe, so we create a unique instance for + each transfer. + + >>> e_tftp.put_file(src='local_file.txt', dest='remote_name.txt') + + :param src: Source file path (on your local machine). + :type src: string + :param dest: Destination path (on the TFTP server). + :type dest: string + + :raises TftpException: If the file cannot be written to the TFTP server. + :raises TftpException: If a TypeError is received from tftpy. + + """ + try: + client = TftpClient(self.ip_address, self.port) + client.upload(input=src, filename=dest) + except TftpException: + if (self.verbose): + traceback.format_exc() + raise + except TypeError: + if (self.verbose): + traceback.format_exc() + raise TftpException("Failed to upload file to TFTP server") + + +# End of file: ./tftp.py diff --git a/cxmanage_api/ubootenv.py b/cxmanage_api/ubootenv.py new file mode 100644 index 0000000..b5b8272 --- /dev/null +++ b/cxmanage_api/ubootenv.py @@ -0,0 +1,255 @@ +# 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 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 + + +ENVIRONMENT_SIZE = 8192 +UBOOTENV_V1_VARIABLES = ["bootcmd_default", "bootcmd_sata", "bootcmd_pxe", + "bootdevice"] +UBOOTENV_V2_VARIABLES = ["bootcmd0", "init_scsi", "bootcmd_scsi", "init_pxe", + "bootcmd_pxe", "devnum"] + + +class UbootEnv: + """Represents a U-Boot Environment. + + >>> from cxmanage_api.ubootenv import UbootEnv + >>> uboot = UbootEnv() + + :param contents: UBootEnvironment contnents. + :type contents: string + + """ + + def __init__(self, contents=None): + """Default constructor for the UbootEnv class.""" + self.variables = {} + + if (contents != None): + if (has_simg(contents)): + contents = get_simg_contents(contents) + + contents = contents.rstrip("%c%c" % (chr(0), chr(255)))[4:] + lines = contents.split(chr(0)) + for line in lines: + part = line.partition("=") + self.variables[part[0]] = part[2] + + def set_boot_order(self, boot_args): + """Sets the boot order specified in the uboot environment. + + >>> uboot.set_boot_order(boot_args=['disk', 'pxe']) + + .. note:: + * Valid Args: + pxe - boot from pxe server\n + disk - boot from default sata device\n + diskX - boot from sata device X\n + diskX:Y - boot from sata device X, partition Y\n + retry - retry last boot device indefinitely\n + reset - reset A9\n + + :param boot_args: Boot args (boot order). A list of strings. + :type boot_args: list + + :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 + + """ + validate_boot_args(boot_args) + if boot_args == self.get_boot_order(): + return + + commands = [] + retry = False + reset = False + + if all(x in self.variables for x in UBOOTENV_V1_VARIABLES): + version = 1 + elif all(x in self.variables for x in UBOOTENV_V2_VARIABLES): + version = 2 + else: + raise Exception("Unrecognized u-boot environment") + + for arg in boot_args: + if arg == "retry": + retry = True + elif arg == "reset": + reset = True + elif version == 1: + if arg == "pxe": + commands.append("run bootcmd_pxe") + elif arg == "disk": + commands.append("run bootcmd_sata") + elif arg.startswith("disk"): + try: + dev, part = map(int, arg[4:].split(":")) + bootdevice = "%i:%i" % (dev, part) + except ValueError: + bootdevice = str(int(arg[4:])) + commands.append("setenv bootdevice %s && run bootcmd_sata" + % bootdevice) + elif version == 2: + if arg == "pxe": + commands.append("run init_pxe && run bootcmd_pxe") + elif arg == "disk": + commands.append("run init_scsi && run bootcmd_scsi") + elif arg.startswith("disk"): + try: + dev, part = map(int, arg[4:].split(":")) + bootdevice = "%i:%i" % (dev, part) + except ValueError: + bootdevice = str(int(arg[4:])) + commands.append( + "setenv devnum %s && run init_scsi && run bootcmd_scsi" + % bootdevice) + + if retry and reset: + raise ValueError("retry and reset are mutually exclusive") + elif retry: + commands[-1] = "while true\ndo\n%s\nsleep 1\ndone" % commands[-1] + elif reset: + commands.append("reset") + + if version == 1: + self.variables["bootcmd_default"] = "; ".join(commands) + else: + self.variables["bootcmd0"] = "; ".join(commands) + + def get_boot_order(self): + """Gets the boot order specified in the uboot environment. + + >>> uboot.get_boot_order() + ['disk', 'pxe'] + + :returns: Boot order for this U-Boot Environment. + :rtype: string + + :raises UnknownBootCmdError: If a boot command is unrecognized. + + """ + boot_args = [] + + if self.variables["bootcmd0"] == "run boot_iter": + for target in self.variables["boot_targets"].split(): + if target == "pxe": + boot_args.append("pxe") + elif target == "scsi": + boot_args.append("disk") + else: + raise UnknownBootCmdError("Unrecognized boot target: %s" + % target) + else: + if "bootcmd_default" in self.variables: + commands = self.variables["bootcmd_default"].split("; ") + else: + commands = self.variables["bootcmd0"].split("; ") + + retry = False + for command in commands: + if command.startswith("while true"): + retry = True + command = command.split("\n")[2] + + if command in ["run bootcmd_pxe", + "run init_pxe && run bootcmd_pxe"]: + boot_args.append("pxe") + elif command in ["run bootcmd_sata", + "run init_scsi && run bootcmd_scsi"]: + boot_args.append("disk") + elif (command.startswith("setenv bootdevice") or + command.startswith("setenv devnum")): + boot_args.append("disk%s" % command.split()[2]) + elif (command == "reset"): + boot_args.append("reset") + break + else: + raise UnknownBootCmdError("Unrecognized boot command: %s" + % command) + + if retry: + boot_args.append("retry") + break + + if not boot_args: + boot_args = ["none"] + + validate_boot_args(boot_args) # sanity check + return boot_args + + def get_contents(self): + """Returns a raw string representation of the uboot environment. + + >>> uboot.get_contents() + 'j4\x88\xb7bootcmd_default=run bootcmd_sata; run bootcmd_pxe ... ' + >>> # + >>> # Output trimmed for brevity ... + >>> # + + :returns: Raw string representation of the UBoot Environment. + :rtype: string + + """ + contents = "" + # Add variables + for variable in self.variables: + contents += "%s=%s\0" % (variable, self.variables[variable]) + contents += "\0" + # Add padding to end + contents += "".join([chr(255) + for _ in range(ENVIRONMENT_SIZE - len(contents) - 4)]) + # Add crc32 to beginning + crc32 = get_crc32(contents, 0xFFFFFFFF) ^ 0xFFFFFFFF + contents = struct.pack("=%s' % PYIPMI_VERSION) + except pkg_resources.DistributionNotFound: + print 'ERROR: cxmanage requires pyipmi version %s'\ + % PYIPMI_VERSION + print 'No existing version was found.' + sys.exit(1) + except pkg_resources.VersionConflict: + version = pkg_resources.require('pyipmi')[0].version + print 'ERROR: cxmanage requires pyipmi version %s' % PYIPMI_VERSION + print 'Current pyipmi version is %s' % version + sys.exit(1) + + + # Check ipmitool version + if 'IPMITOOL_PATH' in os.environ: + args = [os.environ['IPMITOOL_PATH'], '-V'] + else: + args = ['ipmitool', '-V'] + + try: + ipmitool_process = subprocess.Popen(args, stdout=subprocess.PIPE) + ipmitool_version = ipmitool_process.communicate()[0].split()[2] + if pkg_resources.parse_version(ipmitool_version) < \ + pkg_resources.parse_version(IPMITOOL_VERSION): + print 'ERROR: cxmanage requires IPMItool %s or later' \ + % IPMITOOL_VERSION + print 'Current IPMItool version is %s' % ipmitool_version + sys.exit(1) + except OSError: + print 'ERROR: cxmanage requires IPMItool %s or later' \ + % IPMITOOL_VERSION + print 'No existing version was found.' + sys.exit(1) + + +def main(): + """Get args and go""" + for arg in sys.argv[1:]: + if arg in ['-V', '--version']: + print_version() + sys.exit(0) + elif arg[0] != '-': + break + + parser = build_parser() + args = parser.parse_args() + validate_args(args) + + if args.ipmipath: + if os.path.isdir(args.ipmipath): + args.ipmipath = args.ipmipath.rstrip('/') + '/ipmitool' + os.environ['IPMITOOL_PATH'] = args.ipmipath + + check_versions() + + sys.exit(args.func(args)) + + +if __name__ == '__main__': + main() diff --git a/scripts/sol_tabs b/scripts/sol_tabs new file mode 100755 index 0000000..c5cb9fe --- /dev/null +++ b/scripts/sol_tabs @@ -0,0 +1,57 @@ +#!/bin/bash + +# 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. + +node_0_ip=$1 + +# check for hostname arg +if [ $# -eq 0 ] ; then + echo "Please specify a host, I.E. \"sol_tabs 192.168.100.100\"" + exit 1 +fi + +# check for xdotool, wmctrl commands +command -v xdotool &>/dev/null || { echo >&2 "xdotool is required but not installed. Aborting."; exit 1; } +command -v wmctrl &>/dev/null || { echo >&2 "wmctrl is required but not installed. Aborting."; exit 1; } + +for ip in `cxmanage ipinfo $node_0_ip | grep '\([0-9]\{1,3\}\.\)\{3\}[0-9]\{1,3\}' | grep -v from | awk {'print $3'}` +do + echo $ip + WID=$(xprop -root | grep "_NET_ACTIVE_WINDOW(WINDOW)"| awk '{print $5}') + xdotool windowfocus $WID + xdotool key ctrl+shift+t + wmctrl -i -a $WID + sleep 3 + xdotool type " +ipmitool -I lanplus -U admin -P admin -H $ip sol deactivate +ipmitool -I lanplus -U admin -P admin -H $ip sol activate +" +done diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..861a9f5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..bd49b13 --- /dev/null +++ b/setup.py @@ -0,0 +1,54 @@ +# 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 setuptools import setup + +setup( + name='cxmanage', + version='0.8.2', + packages=['cxmanage', 'cxmanage.commands', 'cxmanage_api'], + scripts=['scripts/cxmanage', 'scripts/sol_tabs'], + description='Calxeda Management Utility', + # NOTE: As of right now, the pyipmi version requirement needs to be updated + # at the top of scripts/cxmanage as well. + install_requires=[ + 'tftpy', + 'pexpect', + 'pyipmi>=0.7.1', + 'argparse', + ], + extras_require={ + 'docs': ['sphinx', 'cloud_sptheme'], + }, + classifiers=[ + 'License :: OSI Approved :: BSD License', + 'Programming Language :: Python :: 2.7'] +) -- cgit v1.2.1