summaryrefslogtreecommitdiff
path: root/cxmanage_api
diff options
context:
space:
mode:
Diffstat (limited to 'cxmanage_api')
-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
12 files changed, 4689 insertions, 0 deletions
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)