summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--PKG-INFO12
-rw-r--r--cxmanage.egg-info/PKG-INFO12
-rw-r--r--cxmanage.egg-info/SOURCES.txt31
-rw-r--r--cxmanage.egg-info/dependency_links.txt1
-rw-r--r--cxmanage.egg-info/requires.txt8
-rw-r--r--cxmanage.egg-info/top_level.txt2
-rw-r--r--cxmanage/__init__.py324
-rw-r--r--cxmanage/commands/__init__.py29
-rw-r--r--cxmanage/commands/config.py94
-rw-r--r--cxmanage/commands/fabric.py80
-rw-r--r--cxmanage/commands/fw.py164
-rw-r--r--cxmanage/commands/info.py103
-rw-r--r--cxmanage/commands/ipdiscover.py56
-rw-r--r--cxmanage/commands/ipmitool.py60
-rw-r--r--cxmanage/commands/mc.py47
-rw-r--r--cxmanage/commands/power.py110
-rw-r--r--cxmanage/commands/sensor.py83
-rw-r--r--cxmanage_api/__init__.py65
-rw-r--r--cxmanage_api/crc32.py126
-rw-r--r--cxmanage_api/cx_exceptions.py393
-rw-r--r--cxmanage_api/fabric.py904
-rw-r--r--cxmanage_api/firmware_package.py168
-rw-r--r--cxmanage_api/image.py178
-rw-r--r--cxmanage_api/ip_retriever.py382
-rw-r--r--cxmanage_api/node.py1507
-rw-r--r--cxmanage_api/simg.py239
-rw-r--r--cxmanage_api/tasks.py175
-rw-r--r--cxmanage_api/tftp.py297
-rw-r--r--cxmanage_api/ubootenv.py255
-rwxr-xr-xscripts/cxmanage374
-rwxr-xr-xscripts/sol_tabs57
-rw-r--r--setup.cfg5
-rw-r--r--setup.py54
33 files changed, 6395 insertions, 0 deletions
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 "<stdin>", line 1, in <module>
+ cxmanage_api.cx_exceptions.TimeoutError: My custom exception text!
+
+ :param msg: Exceptions message and details to return to the user.
+ :type msg: string
+ :raised: When 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 "<stdin>", line 1, in <module>
+ 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 "<stdin>", line 1, in <module>
+ 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 "<stdin>", line 1, in <module>
+ cxmanage_api.cx_exceptions.NoFirmwareInfoError: My custom exception text!
+
+ :param msg: Exceptions message and details to return to the user.
+ :type msg: string
+ :raised: When the firmware info cannot be obtained from a node.
+
+ """
+
+ def __init__(self, msg):
+ """Default constructor for the NoFirmwareInfoError class."""
+ super(NoFirmwareInfoError, self).__init__()
+ self.msg = msg
+
+ def __str__(self):
+ """String representation of this Exception class."""
+ return self.msg
+
+
+class SocmanVersionError(Exception):
+ """Raised when there is an error with the users socman version.
+
+ >>> from cxmanage_api.cx_exceptions import SocmanVersionError
+ >>> raise SocmanVersionError('My custom exception text!')
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ 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 "<stdin>", line 1, in <module>
+ 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 "<stdin>", line 1, in <module>
+ 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 "<stdin>", line 1, in <module>
+ 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 "<stdin>", line 1, in <module>
+ 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 "<stdin>", line 1, in <module>
+ 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 "<stdin>", line 1, in <module>
+ 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 "<stdin>", line 1, in <module>
+ 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 "<stdin>", line 1, in <module>
+ 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 "<stdin>", line 1, in <module>
+ 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 <tftp.html>`_
+ :param task_queue: TaskQueue to use for sending commands.
+ :type task_queue: `TaskQueue <tasks.html#cxmanage_api.tasks.TaskQueue>`_
+ :param verbose: Flag to turn on verbose output (cmd/response).
+ :type verbose: boolean
+ :param node: Node type, for dependency integration.
+ :type node: `Node <node.html>`_
+ """
+
+ 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
+ <cxmanage_api.tftp.InternalTftp object at 0x7f5ebbd20b10>
+
+ :return: The tftp server.
+ :rtype: `Tftp <tftp.html>`_
+
+ """
+ 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: <cxmanage_api.node.Node object at 0x2052710>,
+ 1: <cxmanage_api.node.Node object at 0x2052790>,
+ 2: <cxmanage_api.node.Node object at 0x2052850>,
+ 3: <cxmanage_api.node.Node object at 0x2052910>
+ }
+
+ .. 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
+ <cxmanage_api.node.Node object at 0x210d790>
+
+ :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 <tasks.html>`__
+
+ """
+ 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 <tasks.html>`__
+
+ """
+ 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' : <pyipmi.sdr.AnalogSdr object at 0x1a1eb50>,
+ 'DRAM VDD Voltage' : <pyipmi.sdr.AnalogSdr object at 0x1a1ef10>,
+ 'MP Temp 0' : <pyipmi.sdr.AnalogSdr object at 0x1a1ec90>,
+ 'Node Power' : <pyipmi.sdr.AnalogSdr object at 0x1a1ed90>,
+ 'TOP Temp 0' : <pyipmi.sdr.AnalogSdr object at 0x1a1ecd0>,
+ 'TOP Temp 1' : <pyipmi.sdr.AnalogSdr object at 0x1a1ed50>,
+ 'TOP Temp 2' : <pyipmi.sdr.AnalogSdr object at 0x1a1edd0>,
+ 'Temp 0' : <pyipmi.sdr.AnalogSdr object at 0x1a1ead0>,
+ 'Temp 1' : <pyipmi.sdr.AnalogSdr object at 0x1a1ebd0>,
+ 'Temp 2' : <pyipmi.sdr.AnalogSdr object at 0x1a1ec10>,
+ 'Temp 3' : <pyipmi.sdr.AnalogSdr object at 0x1a1ec50>,
+ 'V09 Current' : <pyipmi.sdr.AnalogSdr object at 0x1a1ef90>,
+ 'V09 Voltage' : <pyipmi.sdr.AnalogSdr object at 0x1a1ee90>,
+ 'V18 Current' : <pyipmi.sdr.AnalogSdr object at 0x1a1ef50>,
+ 'V18 Voltage' : <pyipmi.sdr.AnalogSdr object at 0x1a1ee50>,
+ 'VCORE Current' : <pyipmi.sdr.AnalogSdr object at 0x1a1efd0>,
+ 'VCORE Power' : <pyipmi.sdr.AnalogSdr object at 0x1a1ee10>,
+ 'VCORE Voltage' : <pyipmi.sdr.AnalogSdr object at 0x1a1eed0>
+ },
+ #
+ # 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: [<pyipmi.fw.FWInfo object at 0x2808110>, ...],
+ 1: [<pyipmi.fw.FWInfo object at 0x28080d0>, ...],
+ 2: [<pyipmi.fw.FWInfo object at 0x2808090>, ...],
+ 3: [<pyipmi.fw.FWInfo object at 0x7f35540660d0>, ...]
+ }
+
+ :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 <tasks.html>`__
+
+ """
+ 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 <tasks.html>`__
+
+ """
+ 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 <firmware_package.html>`_
+ :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 <tasks.html>`__
+
+ """
+ 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 <firmware_package.html>`_
+ :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 <tasks.html>`__
+
+ """
+ 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: <pyipmi.info.InfoBasicResult object at 0x1f74150>,
+ 1: <pyipmi.info.InfoBasicResult object at 0x1f745d0>,
+ 2: <pyipmi.info.InfoBasicResult object at 0x1f743d0>,
+ 3: <pyipmi.info.InfoBasicResult object at 0x1f74650>
+ }
+
+ .. seealso::
+ `Node.get_versions() <node.html#cxmanage_api.node.Node.get_versions>`_
+
+ :param async: Flag that determines if the command result (dictionary)
+ is returned or a Command object (can get status, etc.).
+ :type async: boolean
+
+ :returns: The basic SoC info for all nodes.
+ :rtype: dictionary or `Task <tasks.html>`__
+
+ """
+ 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() <node.html#cxmanage_api.node.Node.get_versions_dict>`_
+
+ :param async: Flag that determines if the command result (dictionary)
+ is returned or a Task object (can get status, etc.).
+ :type async: boolean
+
+ :returns: The basic SoC info for all nodes.
+ :rtype: dictionary or `Task <tasks.html>`__
+
+ """
+ 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: <cxmanage_api.ubootenv.UbootEnv instance at 0x7fc2d4058098>,
+ 1: <cxmanage_api.ubootenv.UbootEnv instance at 0x7fc2d4058908>,
+ 2: <cxmanage_api.ubootenv.UbootEnv instance at 0x7fc2d40582d8>,
+ 3: <cxmanage_api.ubootenv.UbootEnv instance at 0x7fc2d40589e0>
+ }
+
+ :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 <command.html>`_
+
+ """
+ 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 <command.html>`_
+
+ """
+ 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 <tftp.html>`_
+ :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 <image.html>`_
+ :param ubootenv: UbootEnv for this node. Default cxmanage_api.UbootEnv
+ :type ubootenv: `UbootEnv <ubootenv.html>`_
+
+ """
+
+ 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' : <pyipmi.sdr.AnalogSdr object at 0x1e63890>,
+ 'Temp 0' : <pyipmi.sdr.AnalogSdr object at 0x1e63410>,
+ 'Temp 1' : <pyipmi.sdr.AnalogSdr object at 0x1e638d0>,
+ 'Temp 2' : <pyipmi.sdr.AnalogSdr object at 0x1e63690>,
+ 'Temp 3' : <pyipmi.sdr.AnalogSdr object at 0x1e63950>,
+ 'VCORE Voltage' : <pyipmi.sdr.AnalogSdr object at 0x1e63bd0>,
+ 'TOP Temp 2' : <pyipmi.sdr.AnalogSdr object at 0x1e63ad0>,
+ 'TOP Temp 1' : <pyipmi.sdr.AnalogSdr object at 0x1e63a50>,
+ 'TOP Temp 0' : <pyipmi.sdr.AnalogSdr object at 0x1e639d0>,
+ 'VCORE Current' : <pyipmi.sdr.AnalogSdr object at 0x1e63710>,
+ 'V18 Voltage' : <pyipmi.sdr.AnalogSdr object at 0x1e63b50>,
+ 'V09 Current' : <pyipmi.sdr.AnalogSdr object at 0x1e63990>,
+ 'Node Power' : <pyipmi.sdr.AnalogSdr object at 0x1e63cd0>,
+ 'DRAM VDD Current' : <pyipmi.sdr.AnalogSdr object at 0x1e63910>,
+ 'DRAM VDD Voltage' : <pyipmi.sdr.AnalogSdr object at 0x1e634d0>,
+ 'V18 Current' : <pyipmi.sdr.AnalogSdr object at 0x1e63c50>,
+ 'VCORE Power' : <pyipmi.sdr.AnalogSdr object at 0x1e63c90>,
+ 'V09 Voltage' : <pyipmi.sdr.AnalogSdr object at 0x1e63b90>
+ }
+ >>> # Get ANY sensor that 'contains' the substring of search in it ...
+ >>> node.get_sensors(search='Temp 0')
+ {
+ 'MP Temp 0' : <pyipmi.sdr.AnalogSdr object at 0x1e63810>,
+ 'TOP Temp 0' : <pyipmi.sdr.AnalogSdr object at 0x1e63850>,
+ 'Temp 0' : <pyipmi.sdr.AnalogSdr object at 0x1e63510>
+ }
+
+ :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()
+ [<pyipmi.fw.FWInfo object at 0x2019850>,
+ <pyipmi.fw.FWInfo object at 0x2019b10>,
+ <pyipmi.fw.FWInfo object at 0x2019610>, ...]
+
+ :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 <firmware_package.html>`_
+ :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()
+ <pyipmi.info.InfoBasicResult object at 0x2019b90>
+ >>> # 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()
+ <cxmanage_api.ubootenv.UbootEnv instance at 0x209da28>
+
+ :return: U-Boot Environment object.
+ :rtype: `UBootEnv <ubootenv.html>`_
+
+ """
+ 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)
+ <cxmanage_api.simg.SIMGHeader instance at 0x7f4d1ce9aef0>
+
+ :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 <http://en.wikipedia.org/wiki/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("<I", crc32) + contents
+ return contents
+
+
+def validate_boot_args(boot_args):
+ """ Validate boot arguments. Raises a ValueError if the args are invalid."""
+ for arg in boot_args:
+ if arg in ["retry", "reset", "pxe", "disk", "none"]:
+ continue
+ elif arg.startswith("disk"):
+ try:
+ map(int, arg[4:].split(":"))
+ except ValueError:
+ try:
+ int(arg[4:])
+ except ValueError:
+ raise ValueError("Invalid boot arg: %s" % arg)
+ else:
+ raise ValueError("Invalid boot arg: %s" % arg)
diff --git a/scripts/cxmanage b/scripts/cxmanage
new file mode 100755
index 0000000..101b30b
--- /dev/null
+++ b/scripts/cxmanage
@@ -0,0 +1,374 @@
+#!/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 argparse
+import os
+import pkg_resources
+import subprocess
+import sys
+
+from cxmanage.commands.power import power_command, power_status_command, \
+ power_policy_command, power_policy_status_command
+from cxmanage.commands.mc import mcreset_command
+from cxmanage.commands.fw import fwupdate_command, fwinfo_command
+from cxmanage.commands.sensor import sensor_command
+from cxmanage.commands.fabric import ipinfo_command, macaddrs_command
+from cxmanage.commands.config import config_reset_command, config_boot_command
+from cxmanage.commands.info import info_command
+from cxmanage.commands.ipmitool import ipmitool_command
+from cxmanage.commands.ipdiscover import ipdiscover_command
+
+
+PYIPMI_VERSION = '0.7.1'
+IPMITOOL_VERSION = '1.8.11.0-cx5'
+
+
+PARSER_EPILOG = """examples:
+ cxmanage power status 192.168.1.1 # single host
+ cxmanage power on 192.168.1.1,192.168.1.2 # comma-separated hosts
+ cxmanage info 192.168.1.1-192.168.1.5 # IP range (5 hosts)
+ cxmanage -a sensor temp 192.168.1.1 # all nodes on a fabric
+ cxmanage -a fwupdate package ECX-1000_update.tar.gz 192.168.1.1"""
+
+FWUPDATE_EPILOG = """examples:
+ cxmanage -a fwupdate package ECX-1000_update.tar.gz 192.168.1.1
+ cxmanage -a fwupdate --full package ECX-1000_update.tar.gz 192.168.1.1"""
+
+FWUPDATE_IMAGE_TYPES = ['PACKAGE'] + sorted([
+ 'DEL',
+ 'DEL1',
+ 'S2_ELF',
+ 'SOC_ELF',
+ 'A9_UEFI',
+ 'A9_UBOOT',
+ 'A9_EXEC',
+ 'A9_ELF',
+ 'SOCDATA',
+ 'DTB',
+ 'CDB',
+ 'UBOOTENV',
+ 'SEL',
+ 'BOOT_LOG',
+ 'UEFI_ENV',
+ 'DIAG_ELF',
+])
+
+
+
+def build_parser():
+ """setup the argparse parser"""
+ parser = argparse.ArgumentParser(
+ description='Calxeda Server Management Utility',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=PARSER_EPILOG)
+
+ #global arguments
+ parser.add_argument('-V', '--version', action='store_true',
+ help='Show version information')
+ parser.add_argument('-u', '--user', default='admin',
+ help='Username for login')
+ parser.add_argument('-p', '--password', default='admin',
+ help='Password for login')
+ parser.add_argument('-a', '--all-nodes', action='store_true',
+ help='Send command to all nodes reported by fabric')
+ parser.add_argument('--threads', type=int, metavar='THREAD_COUNT',
+ help='Number of threads to use')
+ parser.add_argument('--command_delay', type=float,
+ metavar='SECONDS', default=0.0,
+ help='Per thread time to delay between issuing commands')
+ parser.add_argument('--force', action='store_true',
+ help='Force the command to run')
+ parser.add_argument('--retry', help='Retry command on multiple times',
+ type=int, default=None, metavar='COUNT')
+ parser.add_argument('--ipmipath', help='Path to ipmitool command',
+ default=None)
+ parser.add_argument('-n', '--nodes', metavar='COUNT', type=int,
+ help='Expected number of nodes')
+ parser.add_argument('-i', '--ids', action='store_true',
+ help='Display node IDs in addition to IP addresses')
+ verbosity = parser.add_mutually_exclusive_group()
+ verbosity.add_argument('-v', '--verbose', action='store_true',
+ help='Verbose output')
+ verbosity.add_argument('-q', '--quiet', action='store_true',
+ help='Quiet output')
+ tftp_type = parser.add_mutually_exclusive_group()
+ tftp_type.add_argument('--internal-tftp', metavar='IP:PORT',
+ help='Host an internal TFTP server listening on ip:port')
+ tftp_type.add_argument('--external-tftp', metavar='IP:PORT',
+ help='Connect to remote TFTP server at ip:port')
+ parser.add_argument('--ecme-tftp-port', type=int, default=5001,
+ metavar='PORT', help='TFTP port of the ECME')
+
+ subparsers = parser.add_subparsers()
+
+ #power command
+ power = subparsers.add_parser('power',
+ help='control server power')
+ power_subs = power.add_subparsers()
+
+ power_on = power_subs.add_parser('on', help='boot the server')
+ power_on.set_defaults(power_mode='on', func=power_command)
+
+ power_off = power_subs.add_parser('off', help='shut the server off')
+ power_off.set_defaults(power_mode='off', func=power_command)
+
+ power_reset = power_subs.add_parser('reset', help='reset the server')
+ power_reset.set_defaults(power_mode='reset', func=power_command)
+
+ power_status = power_subs.add_parser('status',
+ help='get server power status')
+ power_status.set_defaults(func=power_status_command)
+
+ power_policy = power_subs.add_parser('policy',
+ help='set server power policy')
+ power_policy_subs = power_policy.add_subparsers()
+
+ power_policy_always_on = power_policy_subs.add_parser(
+ 'always-on', help='always boot the server by default')
+ power_policy_always_on.set_defaults(policy='always-on',
+ func=power_policy_command)
+ power_policy_always_off = power_policy_subs.add_parser(
+ 'always-off', help='never boot the server by default')
+ power_policy_always_off.set_defaults(policy='always-off',
+ func=power_policy_command)
+ power_policy_previous = power_policy_subs.add_parser(
+ 'previous', help='return to previous power state by default')
+ power_policy_previous.set_defaults(policy='previous',
+ func=power_policy_command)
+ power_policy_status = power_policy_subs.add_parser(
+ 'status', help='get the current power policy')
+ power_policy_status.set_defaults(func=power_policy_status_command)
+
+ #mcreset command
+ mcreset = subparsers.add_parser('mcreset',
+ help='reset the management controller')
+ mcreset.set_defaults(func=mcreset_command)
+
+ #fwupdate command
+ fwupdate = subparsers.add_parser('fwupdate', help='update firmware',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=FWUPDATE_EPILOG)
+ fwupdate.add_argument('image_type', metavar='IMAGE_TYPE',
+ help='image type to use (%s)' % ", ".join(FWUPDATE_IMAGE_TYPES),
+ type=lambda string: string.upper(),
+ choices = FWUPDATE_IMAGE_TYPES)
+ fwupdate.add_argument('filename', help='path to file to upload')
+ fwupdate.add_argument('--full', action='store_true', default=False,
+ help='Update primary AND backup partitions (will reset MC)')
+ fwupdate.add_argument('--partition',
+ help='Specify partition to update', default='INACTIVE',
+ type=lambda string: string.upper(),
+ choices = list([
+ 'FIRST',
+ 'SECOND',
+ 'BOTH',
+ 'OLDEST',
+ 'NEWEST',
+ 'INACTIVE'
+ ]))
+ simg_args = fwupdate.add_mutually_exclusive_group()
+ simg_args.add_argument('--force-simg',
+ help='Force addition of SIMG header',
+ default=False, action='store_true')
+ simg_args.add_argument('--skip-simg',
+ help='Skip addition of SIMG header',
+ default=False, action='store_true')
+ fwupdate.add_argument('--priority',
+ help='Priority for SIMG header', default=None, type=int)
+ fwupdate.add_argument('-d', '--daddr',
+ help='Destination address for SIMG',
+ default=None, type=lambda x : int(x, 16))
+ fwupdate.add_argument('--skip-crc32',
+ help='Skip crc32 calculation for SIMG',
+ default=False, action='store_true')
+ fwupdate.add_argument('--version', dest='fw_version',
+ help='Version for SIMG header', default=None)
+ fwupdate.set_defaults(func=fwupdate_command)
+
+ #fwinfo command
+ fwinfo = subparsers.add_parser('fwinfo', help='get FW info')
+ fwinfo.set_defaults(func=fwinfo_command)
+
+ #sensor command
+ sensor = subparsers.add_parser('sensor',
+ help='read sensor value')
+ sensor.add_argument('sensor_name', help='Sensor name to read',
+ nargs='?', default='')
+ sensor.set_defaults(func=sensor_command)
+
+ #ipinfo command
+ ipinfo = subparsers.add_parser('ipinfo', help='get IP info')
+ ipinfo.set_defaults(func=ipinfo_command)
+
+ #macaddrs command
+ macaddrs = subparsers.add_parser('macaddrs',
+ help='get mac addresses')
+ macaddrs.set_defaults(func=macaddrs_command)
+
+ #config command
+ config = subparsers.add_parser('config', help='configure hosts')
+ config_subs = config.add_subparsers()
+
+ reset = config_subs.add_parser('reset',
+ help='reset to factory default')
+ reset.set_defaults(func=config_reset_command)
+
+ boot = config_subs.add_parser('boot',
+ help='set server boot order')
+ boot.add_argument('boot_order', help='boot order to use', default=[],
+ type=lambda x: [] if x == 'none' else x.split(','))
+ boot.set_defaults(func=config_boot_command)
+
+ #info command
+ info = subparsers.add_parser('info', help='get host info')
+ info.add_argument('info_type', nargs='?',
+ type=lambda string: string.lower(),
+ choices=['basic', 'ubootenv'])
+ info.set_defaults(func=info_command)
+
+ #ipmitool command
+ ipmitool = subparsers.add_parser('ipmitool',
+ help='run an arbitrary ipmitool command')
+ ipmitool.add_argument('-l', '--lanplus',
+ action='store_true', default=False,
+ help='use lanplus')
+ ipmitool.add_argument('ipmitool_args', nargs='+',
+ help='ipmitool arguments')
+ ipmitool.set_defaults(func=ipmitool_command)
+
+ #ipdiscover command
+ ipdiscover = subparsers.add_parser('ipdiscover',
+ help='discover server-side IP addresses')
+ ipdiscover.add_argument('-A', '--aggressive', action='store_true',
+ help='discover IPs aggressively')
+ ipdiscover.add_argument('-U', '--server-user', type=str, default='user1',
+ metavar='USER', help='Server-side Linux username')
+ ipdiscover.add_argument('-P', '--server-password', type=str,
+ default='1Password', metavar='PASSWORD',
+ help='Server-side Linux password')
+ ipdiscover.add_argument('-6', '--ipv6', action='store_true',
+ help='Discover IPv6 addresses')
+ ipdiscover.add_argument('-I', '--interface', type=str, default=None,
+ help='Network interface to check')
+ ipdiscover.set_defaults(func=ipdiscover_command)
+
+ parser.add_argument('hostname',
+ help='nodes to operate on (see examples below)')
+
+ return parser
+
+
+def validate_args(args):
+ """ Bail out if the arguments don't make sense"""
+ if args.threads != None and args.threads < 1:
+ sys.exit('ERROR: --threads must be at least 1')
+ if args.func == fwupdate_command:
+ if args.skip_simg and args.priority:
+ sys.exit('Invalid argument --priority when supplied with --skip-simg')
+ if args.skip_simg and args.daddr:
+ sys.exit('Invalid argument --daddr when supplied with --skip-simg')
+ if args.skip_simg and args.skip_crc32:
+ sys.exit('Invalid argument --skip-crc32 when supplied with --skip-simg')
+ if args.skip_simg and args.fw_version:
+ sys.exit('Invalid argument --version when supplied with --skip-simg')
+
+
+def print_version():
+ """ Print the current version of cxmanage """
+ version = pkg_resources.require('cxmanage')[0].version
+ print "cxmanage version %s" % version
+
+
+def check_versions():
+ """Check versions of dependencies"""
+ # Check pyipmi version
+ try:
+ pkg_resources.require('pyipmi>=%s' % PYIPMI_VERSION)
+ except pkg_resources.DistributionNotFound:
+ print 'ERROR: cxmanage requires pyipmi version %s'\
+ % PYIPMI_VERSION
+ print 'No existing version was found.'
+ sys.exit(1)
+ except pkg_resources.VersionConflict:
+ version = pkg_resources.require('pyipmi')[0].version
+ 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']
+)