# Copyright (c) 2012, Calxeda Inc. # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of Calxeda Inc. nor the names of its contributors # may be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH # DAMAGE. from cxmanage_api.tasks import DEFAULT_TASK_QUEUE from cxmanage_api.tftp import InternalTftp from cxmanage_api.node import Node as NODE from cxmanage_api.cx_exceptions import CommandFailedError class Fabric(object): """ The Fabric class provides management of multiple nodes. >>> from cxmanage_api.fabric import Fabric >>> fabric = Fabric('10.20.1.9') :param ip_address: The ip_address of ANY known node for the Fabric. :type ip_address: string :param username: The login username credential. [Default admin] :type username: string :param password: The login password credential. [Default admin] :type password: string :param tftp: Tftp server to facilitate IPMI command responses. :type tftp: `Tftp `_ :param task_queue: TaskQueue to use for sending commands. :type task_queue: `TaskQueue `_ :param verbose: Flag to turn on verbose output (cmd/response). :type verbose: boolean :param node: Node type, for dependency integration. :type node: `Node `_ """ def __init__(self, ip_address, username="admin", password="admin", tftp=None, ecme_tftp_port=5001, task_queue=None, verbose=False, node=None): """Default constructor for the Fabric class.""" self.ip_address = ip_address self.username = username self.password = password self._tftp = tftp self.ecme_tftp_port = ecme_tftp_port self.task_queue = task_queue self.verbose = verbose self.node = node self._nodes = {} if (not self.node): self.node = NODE if (not self.task_queue): self.task_queue = DEFAULT_TASK_QUEUE if (not self._tftp): self._tftp = InternalTftp() def __eq__(self, other): """__eq__() override.""" return (isinstance(other, Fabric) and self.nodes == other.nodes) def __hash__(self): """__hash__() override.""" return hash(tuple(self.nodes.iteritems())) def __str__(self): """__str__() override.""" return 'Fabric Node 0: %s (%d nodes)' % (self.nodes[0].ip_address, len(self.nodes)) @property def tftp(self): """Returns the tftp server for this Fabric. >>> fabric.tftp :return: The tftp server. :rtype: `Tftp `_ """ return self._tftp @tftp.setter def tftp(self, value): """ Set the TFTP server for this fabric (and all nodes) """ self._tftp = value if not self._nodes: return for node in self.nodes.values(): node.tftp = value @property def nodes(self): """List of nodes in this fabric. >>> fabric.nodes { 0: , 1: , 2: , 3: } .. note:: * Fabric nodes are lazily initialized. :returns: A mapping of node ids to node objects. :rtype: dictionary """ if not self._nodes: self._discover_nodes(self.ip_address) return self._nodes @property def primary_node(self): """The node to use for fabric config operations. Today, this is always node 0. >>> fabric.primary_node :return: Node object for primary node :rtype: Node object """ return self.nodes[0] def get_mac_addresses(self): """Gets MAC addresses from all nodes. >>> fabric.get_mac_addresses() { 0: ['fc:2f:40:3b:ec:40', 'fc:2f:40:3b:ec:41', 'fc:2f:40:3b:ec:42'], 1: ['fc:2f:40:91:dc:40', 'fc:2f:40:91:dc:41', 'fc:2f:40:91:dc:42'], 2: ['fc:2f:40:ab:f7:14', 'fc:2f:40:ab:f7:15', 'fc:2f:40:ab:f7:16'], 3: ['fc:2f:40:88:b3:6c', 'fc:2f:40:88:b3:6d', 'fc:2f:40:88:b3:6e'] } :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean :return: The MAC addresses for each node. :rtype: dictionary """ return self.primary_node.get_fabric_macaddrs() def get_uplink_info(self): """Gets the fabric uplink info. >>> fabric.get_uplink_info() { 0: {0: 0, 1: 0, 2: 0} 1: {0: 0, 1: 0, 2: 0} 2: {0: 0, 1: 0, 2: 0} 3: {0: 0, 1: 0, 2: 0} } :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean :return: The uplink info for each node. :rtype: dictionary """ return self.primary_node.get_fabric_uplink_info() def get_power(self, async=False): """Returns the power status for all nodes. >>> fabric.get_power() {0: False, 1: False, 2: False, 3: False} :param async: Flag that determines if the command result (dictionary) is returned or a Command object (for cmd status, etc.). :type async: boolean :return: The power status of each node. :rtype: dictionary or `Task `__ """ return self._run_on_all_nodes(async, "get_power") def set_power(self, mode, async=False): """Send an IPMI power command to all nodes. >>> # On ... >>> fabric.set_power(mode='on') >>> # Off ... >>> fabric.set_power(mode='off') >>> # Sanity check ... >>> fabric.get_power() {0: False, 1: False, 2: False, 3: False} :param mode: Mode to set the power to (for all nodes). :type mode: string :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean """ self._run_on_all_nodes(async, "set_power", mode) def get_power_policy(self, async=False): """Gets the power policy from all nodes. >>> fabric.get_power_policy() {0: 'always-on', 1: 'always-on', 2: 'always-on', 3: 'always-on'} :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean :return: The power policy for all nodes on this fabric. :rtype: dictionary or `Task `__ """ return self._run_on_all_nodes(async, "get_power_policy") def set_power_policy(self, state, async=False): """Sets the power policy on all nodes. >>> fabric.set_power_policy(state='always-off') >>> # Check to see if it took ... >>> fabric.get_power_policy() {0: 'always-off', 1: 'always-off', 2: 'always-off', 3: 'always-off'} :param state: State to set the power policy to for all nodes. :type state: string :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean """ self._run_on_all_nodes(async, "set_power_policy", state) def mc_reset(self, wait=False, async=False): """Resets the management controller on all nodes. >>> fabric.mc_reset() :param wait: Wait for the nodes to come back up. :type wait: boolean :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean """ self._run_on_all_nodes(async, "mc_reset", wait) def get_sensors(self, search="", async=False): """Gets sensors from all nodes. >>> fabric.get_sensors() { 0: { 'DRAM VDD Current' : , 'DRAM VDD Voltage' : , 'MP Temp 0' : , 'Node Power' : , 'TOP Temp 0' : , 'TOP Temp 1' : , 'TOP Temp 2' : , 'Temp 0' : , 'Temp 1' : , 'Temp 2' : , 'Temp 3' : , 'V09 Current' : , 'V09 Voltage' : , 'V18 Current' : , 'V18 Voltage' : , 'VCORE Current' : , 'VCORE Power' : , 'VCORE Voltage' : }, # # Output trimmed for brevity ... The output would be the same # (format) for the remaining 3 ECMEs on this system. # }, .. note:: * Output condensed for brevity. * If the name parameter is not specified, all sensors are returned. :param name: Name of the sensor to get. (for all nodes) :type name: string :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean """ return self._run_on_all_nodes(async, "get_sensors", search) def get_firmware_info(self, async=False): """Gets the firmware info from all nodes. >>> fabric.get_firmware_info() { 0: [, ...], 1: [, ...], 2: [, ...], 3: [, ...] } :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean :return: THe firmware info for all nodes. :rtype: dictionary or `Task `__ """ return self._run_on_all_nodes(async, "get_firmware_info") def get_firmware_info_dict(self, async=False): """Gets the firmware info from all nodes. >>> fabric.get_firmware_info_dict() {0: [ # # Each dictionary (in order) in this list represents the # corresponding partition information # {# Partition 0 'daddr' : '20029000', 'flags' : 'fffffffd', 'in_use' : 'Unknown', 'offset' : '00000000', 'partition' : '00', 'priority' : '0000000c', 'size' : '00005000', 'type' : '02 (S2_ELF)', 'version' : 'v0.9.1' }, # Partitions 1 - 17 ], # # Output trimmed for brevity ... The remaining Nodes in the Fabric # would display all the partition format in the same manner. # } :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean :return: The firmware info for all nodes. :rtype: dictionary or `Task `__ """ return self._run_on_all_nodes(async, "get_firmware_info_dict") def is_updatable(self, package, partition_arg="INACTIVE", priority=None, async=False): """Checks to see if all nodes can be updated with this fw package. >>> fabric.is_updatable(package=fwpkg) {0: True, 1: True, 2: True, 3: True} :param package: Firmware package to test for updating. :type package: `FirmwarePackage `_ :param partition: Partition to test for updating. :type partition: string :param priority: SIMG Header priority. :type priority: integer :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean :return: Whether or not a node can be updated with the specified firmware package. :rtype: dictionary or `Task `__ """ return self._run_on_all_nodes(async, "is_updatable", package, partition_arg, priority) def update_firmware(self, package, partition_arg="INACTIVE", priority=None, async=False): """Updates the firmware on all nodes. >>> fabric.update_firmware(package=fwpkg) :param package: Firmware package to update to. :type package: `FirmwarePackage `_ :param partition_arg: Which partition to update. :type partition_arg: string :param priority: SIMG header Priority setting. :type priority: integer :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean """ self._run_on_all_nodes(async, "update_firmware", package, partition_arg, priority) def config_reset(self, async=False): """Resets the configuration on all nodes to factory defaults. >>> fabric.config_reset() {0: None, 1: None, 2: None, 3: None} :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean """ self._run_on_all_nodes(async, "config_reset") def set_boot_order(self, boot_args, async=False): """Sets the boot order on all nodes. >>> fabric.set_boot_order(boot_args=['pxe', 'disk']) :param boot_args: Boot order list. :type boot_args: list :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean """ self._run_on_all_nodes(async, "set_boot_order", boot_args) def get_boot_order(self, async=False): """Gets the boot order from all nodes. >>> fabric.get_boot_order() { 0: ['disk', 'pxe'], 1: ['disk', 'pxe'], 2: ['disk', 'pxe'], 3: ['disk', 'pxe'] } :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean :returns: The boot order of each node on this fabric. :rtype: dictionary or `Task `__ """ return self._run_on_all_nodes(async, "get_boot_order") def get_versions(self, async=False): """Gets the version info from all nodes. >>> fabric.get_versions() { 0: , 1: , 2: , 3: } .. seealso:: `Node.get_versions() `_ :param async: Flag that determines if the command result (dictionary) is returned or a Command object (can get status, etc.). :type async: boolean :returns: The basic SoC info for all nodes. :rtype: dictionary or `Task `__ """ return self._run_on_all_nodes(async, "get_versions") def get_versions_dict(self, async=False): """Gets the version info from all nodes. >>> fabric.get_versions_dict() {0: { 'a9boot_version' : 'v2012.10.16', 'bootlog_version' : 'v0.9.1-39-g7e10987', 'build_number' : '7E10987C', 'card' : 'EnergyCard X02', 'cdb_version' : 'v0.9.1-39-g7e10987', 'dtb_version' : 'v3.6-rc1_cx_2012.10.02', 'header' : 'Calxeda SoC (0x0096CD)', 'soc_version' : 'v0.9.1', 'stage2_version' : 'v0.9.1', 'timestamp' : '1352911670', 'uboot_version' : 'v2012.07_cx_2012.10.29', 'ubootenv_version' : 'v2012.07_cx_2012.10.29', 'version' : 'ECX-1000-v1.7.1' }, # # Output trimmed for brevity ... Each remaining Nodes get_versions # dictionary would be printed. # } .. seealso:: `Node.get_versions_dict() `_ :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean :returns: The basic SoC info for all nodes. :rtype: dictionary or `Task `__ """ return self._run_on_all_nodes(async, "get_versions_dict") def ipmitool_command(self, ipmitool_args, asynchronous=False): """Run an arbitrary IPMItool command on all nodes. >>> # Gets eth0's MAC Address for each node ... >>> fabric.ipmitool_command(['cxoem', 'fabric', 'get', 'macaddr', >>> ...'interface', '0']) { 0: 'fc:2f:40:3b:ec:40', 1: 'fc:2f:40:91:dc:40', 2: 'fc:2f:40:ab:f7:14', 3: 'fc:2f:40:88:b3:6c' } :param ipmitool_args: Arguments to pass on to the ipmitool command. :type ipmitool_args: list :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean :returns: IPMI command response. :rtype: string """ return self._run_on_all_nodes(asynchronous, "ipmitool_command", ipmitool_args) def get_ubootenv(self, async=False): """Gets the u-boot environment from all nodes. >>> fabric.get_ubootenv() { 0: , 1: , 2: , 3: } :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean :returns: UBootEnvironment objects for all nodes. :rtype: dictionary or `Task `_ """ return self._run_on_all_nodes(async, "get_ubootenv") def get_server_ip(self, interface=None, ipv6=False, user="user1", password="1Password", aggressive=False, async=False): """Get the server IP address from all nodes. The nodes must be powered on for this to work. >>> fabric.get_server_ip() { 0: '192.168.100.100', 1: '192.168.100.101', 2: '192.168.100.102', 3: '192.168.100.103' } :param interface: Network interface to check (e.g. eth0). :type interface: string :param ipv6: Return an IPv6 address instead of IPv4. :type ipv6: boolean :param user: Linux username. :type user: string :param password: Linux password. :type password: string :param aggressive: Discover the IP aggressively (may power cycle node). :type aggressive: boolean :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean :return: Server IP addresses for all nodes.. :rtype: dictionary or `Task `_ """ return self._run_on_all_nodes(async, "get_server_ip", interface, ipv6, user, password, aggressive) def get_ipsrc(self): """Return the ipsrc for the fabric. >>> fabric.get_ipsrc() 2 :return: 1 for static, 2 for DHCP :rtype: integer """ return self.primary_node.bmc.fabric_config_get_ip_src() def set_ipsrc(self, ipsrc_mode): """Set the ipsrc for the fabric. >>> fabric.set_ipsrc(2) :param ipsrc_mode: 1 for static, 2 for DHCP :type ipsrc_mode: integer """ self.primary_node.bmc.fabric_config_set_ip_src(ipsrc_mode) def apply_factory_default_config(self): """Sets the fabric config to factory default >>> fabric.apply_factory_default_config() """ self.primary_node.bmc.fabric_config_factory_default() def get_ipaddr_base(self): """The base IPv4 address for a range of static IP addresses used for the nodes in the fabric >>> fabric.get_ipaddr_base() '192.168.100.1' :return: The first IP address in the range of static IP addresses :rtype: string """ return self.primary_node.bmc.fabric_config_get_ip_addr_base() def update_config(self): """Push out updated configuration data for all nodes in the fabric. >>> fabric.update_config() """ self.primary_node.bmc.fabric_config_update_config() def get_linkspeed(self): """Get the global linkspeed for the fabric. In the partition world this means the linkspeed for Configuration 0, Partition 0, Profile 0. >>> fabric.get_linkspeed() 2.5 :return: Linkspeed for the fabric. :rtype: float """ return self.primary_node.bmc.fabric_config_get_linkspeed() def set_linkspeed(self, linkspeed): """Set the global linkspeed for the fabric. In the partition world this means the linkspeed for Configuration 0, Partition 0, Profile 0. >>> fabric.set_linkspeed(10) :param linkspeed: Linkspeed specified in Gbps. :type linkspeed: float """ self.primary_node.bmc.fabric_config_set_linkspeed(linkspeed) def add_macaddr(self, nodeid, iface, macaddr): """Add a new macaddr to a node/interface in the fabric. >>> fabric.add_macaddr(3, 1, "66:55:44:33:22:11") :param nodeid: Node id to which the macaddr is to be added :type nodeid: integer :param iface: interface on the node to which the macaddr is to be added :type iface: integer :param macaddr: mac address to be added :type macaddr: string """ self.primary_node.bmc.fabric_add_macaddr(nodeid=nodeid, iface=iface, macaddr=macaddr) def rm_macaddr(self, nodeid, iface, macaddr): """Remove a macaddr to a node/interface in the fabric. >>> fabric.rm_macaddr(3, 1, "66:55:44:33:22:11") :param nodeid: Node id from which the macaddr is to be remove :type nodeid: integer :param iface: interface on the node from which the macaddr is to be removed :type iface: integer :param macaddr: mac address to be removed :type macaddr: string """ self.primary_node.bmc.fabric_rm_macaddr(nodeid=nodeid, iface=iface, macaddr=macaddr) def get_linkspeed_policy(self): """Get the global linkspeed policy for the fabric. In the partition world this means the linkspeed for Configuration 0, Partition 0, Profile 0. >>> fabric.get_linkspeed_policy() 1 :return: Linkspeed Policy for the fabric. :rtype: integer """ return self.primary_node.bmc.fabric_config_get_linkspeed_policy() def set_linkspeed_policy(self, ls_policy): """Set the global linkspeed policy for the fabric. In the partition world this means the linkspeed policy for Configuration 0, Partition 0, Profile 0. >>> fabric.set_linkspeed_policy(1) :param linkspeed: Linkspeed Policy. 0: Fixed, 1: Topological :type linkspeed: integer """ self.primary_node.bmc.fabric_config_set_linkspeed_policy(ls_policy) def get_link_users_factor(self): """Get the global link users factor for the fabric. In the partition world this means the link users factor for Configuration 0, Partition 0, Profile 0. >>> fabric.get_link_users_factor() 1 :return: Link users factor for the fabric. :rtype: integer """ return self.primary_node.bmc.fabric_config_get_link_users_factor() def set_link_users_factor(self, lu_factor): """Set the global link users factor for the fabric. In the partition world this means the link users factor for Configuration 0, Partition 0, Profile 0. >>> fabric.set_link_users_factor(10) :param lu_factor: Multiplying factor for topological linkspeeds :type lu_factor: integer """ self.primary_node.bmc.fabric_config_set_link_users_factor(lu_factor) def get_uplink(self, iface=0): """Get the uplink for an interface to xmit a packet out of the cluster. >>> fabric.get_uplink(0) 0 :param iface: The interface for the uplink. :type iface: integer :return: The uplink iface is using. :rtype: integer """ return self.primary_node.bmc.fabric_config_get_uplink(iface=iface) def set_uplink(self, uplink=0, iface=0): """Set the uplink for an interface to xmit a packet out of the cluster. >>> fabric.set_uplink(0,0) :param uplink: The uplink to set. :type uplink: integer :param iface: The interface for the uplink. :type iface: integer """ self.primary_node.bmc.fabric_config_set_uplink(uplink=uplink, iface=iface) def get_link_stats(self, link=0, async=False): """Get the link_stats for each node in the fabric. :param link: The link to get stats for (0-4). :type link: integer :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean :returns: The link_stats for each link on each node. :rtype: dictionary """ return self._run_on_all_nodes(async, "get_link_stats", link) def get_linkmap(self, async=False): """Get the linkmap for each node in the fabric. :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean :returns: The linkmap for each node. :rtype: dectionary """ return self._run_on_all_nodes(async, "get_linkmap") def get_routing_table(self, async=False): """Get the routing_table for the fabric. :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean :returns: The routing_table for the fabric. :rtype: dictionary """ return self._run_on_all_nodes(async, "get_routing_table") def get_depth_chart(self, async=False): """Get the depth_chart for the fabric. :param async: Flag that determines if the command result (dictionary) is returned or a Task object (can get status, etc.). :type async: boolean :returns: The depth_chart for the fabric. :rtype: dictionary """ return self._run_on_all_nodes(async, "get_depth_chart") def _run_on_all_nodes(self, async, name, *args): """Start a command on all nodes.""" tasks = {} for node_id, node in self.nodes.iteritems(): tasks[node_id] = self.task_queue.put(getattr(node, name), *args) if async: return tasks else: results = {} errors = {} for node_id, task in tasks.iteritems(): task.join() if task.status == "Completed": results[node_id] = task.result else: errors[node_id] = task.error if errors: raise CommandFailedError(results, errors) return results def _discover_nodes(self, ip_address, username="admin", password="admin"): """Gets the nodes of this fabric by pulling IP info from a BMC.""" node = self.node(ip_address=ip_address, username=username, password=password, tftp=self.tftp, ecme_tftp_port=self.ecme_tftp_port, verbose=self.verbose) ipinfo = node.get_fabric_ipinfo() for node_id, node_address in ipinfo.iteritems(): self._nodes[node_id] = self.node(ip_address=node_address, username=username, password=password, tftp=self.tftp, ecme_tftp_port=self.ecme_tftp_port, verbose=self.verbose) self._nodes[node_id].node_id = node_id # End of file: ./fabric.py