diff options
-rw-r--r-- | cxmanage_api/cli/__init__.py | 26 | ||||
-rw-r--r-- | cxmanage_api/cli/commands/ipdiscover.py | 5 | ||||
-rw-r--r-- | cxmanage_api/credentials.py | 56 | ||||
-rw-r--r-- | cxmanage_api/fabric.py | 40 | ||||
-rw-r--r-- | cxmanage_api/node.py | 45 | ||||
-rw-r--r-- | cxmanage_api/tests/dummy_node.py | 3 | ||||
-rw-r--r-- | cxmanage_api/tests/fabric_test.py | 9 | ||||
-rw-r--r-- | cxmanage_api/tests/test_credentials.py | 69 | ||||
-rw-r--r-- | pylint.rc | 2 | ||||
-rwxr-xr-x | run_tests | 5 | ||||
-rwxr-xr-x | scripts/cxmanage | 9 | ||||
-rwxr-xr-x | scripts/cxmux | 5 |
12 files changed, 203 insertions, 71 deletions
diff --git a/cxmanage_api/cli/__init__.py b/cxmanage_api/cli/__init__.py index 5acb0c9..033edec 100644 --- a/cxmanage_api/cli/__init__.py +++ b/cxmanage_api/cli/__init__.py @@ -100,9 +100,20 @@ def get_nodes(args, tftp, verify_prompt=False): for entry in args.hostname.split(','): hosts.extend(parse_host_entry(entry)) - nodes = [Node(ip_address=x, username=args.user, password=args.password, - tftp=tftp, ecme_tftp_port=args.ecme_tftp_port, - verbose=args.verbose) for x in hosts] + credentials = { + "ecme_username": args.user, + "ecme_password": args.password, + "linux_username": args.linux_username, + "linux_password": args.linux_password + } + + nodes = [ + Node( + ip_address=x, credentials=credentials, tftp=tftp, + ecme_tftp_port=args.ecme_tftp_port, verbose=args.verbose + ) + for x in hosts + ] if args.all_nodes: if not args.quiet: @@ -116,10 +127,11 @@ def get_nodes(args, tftp, verify_prompt=False): for node in nodes: if node in results: for node_id, ip_address in sorted(results[node].iteritems()): - new_node = Node(ip_address=ip_address, username=args.user, - password=args.password, tftp=tftp, - ecme_tftp_port=args.ecme_tftp_port, - verbose=args.verbose) + new_node = Node( + ip_address=ip_address, credentials=credentials, + tftp=tftp, ecme_tftp_port=args.ecme_tftp_port, + verbose=args.verbose + ) new_node.node_id = node_id if not new_node in all_nodes: all_nodes.append(new_node) diff --git a/cxmanage_api/cli/commands/ipdiscover.py b/cxmanage_api/cli/commands/ipdiscover.py index fd21546..98a0155 100644 --- a/cxmanage_api/cli/commands/ipdiscover.py +++ b/cxmanage_api/cli/commands/ipdiscover.py @@ -42,8 +42,9 @@ def ipdiscover_command(args): if not args.quiet: print 'Getting server-side IP addresses...' - results, errors = run_command(args, nodes, 'get_server_ip', args.interface, - args.ipv6, args.server_user, args.server_password, args.aggressive) + results, errors = run_command( + args, nodes, 'get_server_ip', args.interface, args.ipv6, args.aggressive + ) if results: node_strings = get_node_strings(args, results, justify=True) diff --git a/cxmanage_api/credentials.py b/cxmanage_api/credentials.py new file mode 100644 index 0000000..8709bf9 --- /dev/null +++ b/cxmanage_api/credentials.py @@ -0,0 +1,56 @@ +# Copyright (c) 2012-2013, 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. + + +class Credentials(object): + """ Container for login credentials """ + defaults = { + "ecme_username": "admin", + "ecme_password": "admin", + "linux_username": "user1", + "linux_password": "1Password" + } + + def __init__(self, base=None, **kwargs): + self.__dict__.update(self.defaults) + if isinstance(base, Credentials): + self.__dict__.update(vars(base)) + elif base != None: + self.__dict__.update(base) + self.__dict__.update(kwargs) + + for key in self.__dict__: + if not key in self.defaults: + raise ValueError("Invalid credential key: %s" % key) + + def __repr__(self): + return "Credentials(%s)" % (", ".join( + "%r: %r" % (key, value) for (key, value) in vars(self).items() + )) diff --git a/cxmanage_api/fabric.py b/cxmanage_api/fabric.py index bd05a10..4a84743 100644 --- a/cxmanage_api/fabric.py +++ b/cxmanage_api/fabric.py @@ -38,6 +38,7 @@ import re 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.credentials import Credentials from cxmanage_api.cx_exceptions import CommandFailedError, TimeoutError, \ IpmiError, TftpException, ParseError @@ -51,10 +52,8 @@ class Fabric(object): :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 credentials: Login credentials for ECME/Linux + :type credentials: Credentials :param tftp: Tftp server to facilitate IPMI command responses. :type tftp: `Tftp <tftp.html>`_ :param task_queue: TaskQueue to use for sending commands. @@ -113,13 +112,12 @@ class Fabric(object): return function # pylint: disable=R0913 - def __init__(self, ip_address, username="admin", password="admin", - tftp=None, ecme_tftp_port=5001, task_queue=None, - verbose=False, node=None): + def __init__(self, ip_address, credentials=None, 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.credentials = Credentials(credentials) self._tftp = tftp self.ecme_tftp_port = ecme_tftp_port self.task_queue = task_queue @@ -218,16 +216,15 @@ class Fabric(object): """Returns a dictionary of nodes reported by the primary node IP""" new_nodes = {} root_node = self.node( - ip_address=self.ip_address, username=self.username, - password=self.password, tftp=self.tftp, - ecme_tftp_port=self.ecme_tftp_port, verbose=self.verbose + ip_address=self.ip_address, credentials=self.credentials, + tftp=self.tftp, ecme_tftp_port=self.ecme_tftp_port, + verbose=self.verbose ) ipinfo = root_node.get_fabric_ipinfo() for node_id, node_address in ipinfo.items(): node = self.node( - ip_address=node_address, username=self.username, - password=self.password, tftp=self.tftp, - ecme_tftp_port=self.ecme_tftp_port, + ip_address=node_address, credentials=self.credentials, + tftp=self.tftp, ecme_tftp_port=self.ecme_tftp_port, verbose=self.verbose ) node.node_id = node_id @@ -934,8 +931,8 @@ class Fabric(object): return self._run_on_all_nodes(async, "get_ubootenv") # pylint: disable=R0913 - def get_server_ip(self, interface=None, ipv6=False, user="user1", - password="1Password", aggressive=False, async=False): + def get_server_ip(self, interface=None, ipv6=False, aggressive=False, + async=False): """Get the server IP address from all nodes. The nodes must be powered on for this to work. @@ -951,10 +948,6 @@ class Fabric(object): :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) @@ -965,8 +958,9 @@ class Fabric(object): :rtype: dictionary or `Task <command.html>`_ """ - return self._run_on_all_nodes(async, "get_server_ip", interface, ipv6, - user, password, aggressive) + return self._run_on_all_nodes( + async, "get_server_ip", interface, ipv6, aggressive + ) def get_ipsrc(self): """Return the ipsrc for the fabric. diff --git a/cxmanage_api/node.py b/cxmanage_api/node.py index 784f7f5..f247495 100644 --- a/cxmanage_api/node.py +++ b/cxmanage_api/node.py @@ -51,6 +51,7 @@ 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.decorators import retry +from cxmanage_api.credentials import Credentials from cxmanage_api.cx_exceptions import TimeoutError, NoSensorError, \ SocmanVersionError, FirmwareConfigError, PriorityIncrementError, \ NoPartitionError, TransferFailure, ImageSizeError, \ @@ -68,10 +69,8 @@ class Node(object): :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 credentials: Login credentials for ECME/Linux + :type credentials: Credentials :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). @@ -85,9 +84,9 @@ class Node(object): """ # pylint: disable=R0913 - 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): + def __init__(self, ip_address, credentials=None, 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.default() @@ -103,14 +102,15 @@ class Node(object): ipretriever = IPRETRIEVER self.ip_address = ip_address - self.username = username - self.password = password + self.credentials = Credentials(credentials) 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.bmc = make_bmc( + bmc, hostname=ip_address, username=self.credentials.ecme_username, + password=self.credentials.ecme_password, verbose=verbose + ) self.image = image self.ubootenv = ubootenv self.ipretriever = ipretriever @@ -1143,8 +1143,11 @@ communication. else: command = ["ipmitool"] - command += ["-U", self.username, "-P", self.password, "-H", - self.ip_address] + command += [ + "-U", self.credentials.ecme_username, + "-P", self.credentials.ecme_password, + "-H", self.ip_address + ] command += ipmitool_args if (self.verbose): @@ -1481,8 +1484,7 @@ communication. return results - def get_server_ip(self, interface=None, ipv6=False, user="user1", - password="1Password", aggressive=False): + def get_server_ip(self, interface=None, ipv6=False, aggressive=False): """Get the IP address of the Linux server. The server must be powered on for this to work. @@ -1493,10 +1495,6 @@ communication. :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 @@ -1509,9 +1507,12 @@ 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 = self.ipretriever( + self.ip_address, aggressive=aggressive, verbosity=verbosity, + server_user=self.credentials.linux_username, + server_password=self.credentials.linux_password, + interface=interface, ipv6=ipv6, bmc=self.bmc + ) retriever.run() return retriever.server_ip diff --git a/cxmanage_api/tests/dummy_node.py b/cxmanage_api/tests/dummy_node.py index 02af2c2..6d54646 100644 --- a/cxmanage_api/tests/dummy_node.py +++ b/cxmanage_api/tests/dummy_node.py @@ -128,8 +128,7 @@ class DummyNode(Dummy): return {} # pylint: disable=R0913 - def get_server_ip(self, interface=None, ipv6=False, user="user1", - password="1Password", aggressive=False): + def get_server_ip(self, interface=None, ipv6=False, aggressive=False): """Simulate get_server_ip(). """ return "192.168.200.1" diff --git a/cxmanage_api/tests/fabric_test.py b/cxmanage_api/tests/fabric_test.py index 3863186..757edc1 100644 --- a/cxmanage_api/tests/fabric_test.py +++ b/cxmanage_api/tests/fabric_test.py @@ -192,12 +192,11 @@ class FabricTest(unittest.TestCase): def test_get_server_ip(self): """ Test get_server_ip command """ - self.fabric.get_server_ip("interface", "ipv6", "user", "password", - "aggressive") + self.fabric.get_server_ip("interface", "ipv6", "aggressive") for node in self.nodes: - self.assertEqual(node.method_calls, [call.get_server_ip( - "interface", "ipv6", "user", "password", "aggressive" - )]) + self.assertEqual(node.method_calls, + [call.get_server_ip("interface", "ipv6", "aggressive")] + ) def test_failed_command(self): """ Test a failed command """ diff --git a/cxmanage_api/tests/test_credentials.py b/cxmanage_api/tests/test_credentials.py new file mode 100644 index 0000000..19de536 --- /dev/null +++ b/cxmanage_api/tests/test_credentials.py @@ -0,0 +1,69 @@ +# Copyright (c) 2012-2013, 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 unittest + +from cxmanage_api.credentials import Credentials + + +class TestCredentials(unittest.TestCase): + """ Unit tests for the Credentials class """ + def test_default(self): + """ Test default Credentials object """ + creds = Credentials() + self.assertEqual(vars(creds), Credentials.defaults) + + def test_from_dict(self): + """ Test Credentials instantiated with a dict """ + creds = Credentials({"linux_password": "foo"}) + expected = dict(Credentials.defaults) + expected["linux_password"] = "foo" + self.assertEqual(vars(creds), expected) + + def test_from_kwargs(self): + """ Test Credentials instantiated with kwargs """ + creds = Credentials(linux_password="foo") + expected = dict(Credentials.defaults) + expected["linux_password"] = "foo" + self.assertEqual(vars(creds), expected) + + def test_from_credentials(self): + """ Test Credentials instantiated with other Credentials """ + creds = Credentials(Credentials(linux_password="foo")) + expected = dict(Credentials.defaults) + expected["linux_password"] = "foo" + self.assertEqual(vars(creds), expected) + + def test_fails_on_invalid(self): + """ Test that we don't allow unrecognized credentials """ + with self.assertRaises(ValueError): + Credentials({"desire_to_keep_going": "Very Low"}) + with self.assertRaises(ValueError): + Credentials(magical_mystery_cure="Writing silly strings!") @@ -150,7 +150,7 @@ ignore-mixin-members=yes # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). -ignored-classes=SQLObject +ignored-classes=SQLObject,Credentials # When zope mode is activated, add a predefined set of Zope acquired attributes # to generated-members. @@ -35,9 +35,10 @@ import unittest import xmlrunner from cxmanage_api.tests import tftp_test, image_test, node_test, fabric_test, \ - tasks_test, dummy_test + tasks_test, dummy_test, test_credentials test_modules = [ - tftp_test, image_test, node_test, fabric_test, tasks_test, dummy_test + tftp_test, image_test, node_test, fabric_test, tasks_test, dummy_test, + test_credentials ] def main(): diff --git a/scripts/cxmanage b/scripts/cxmanage index d87bb41..c9e64f8 100755 --- a/scripts/cxmanage +++ b/scripts/cxmanage @@ -111,6 +111,10 @@ def build_parser(): help='Username for login') parser.add_argument('-p', '--password', default='admin', help='Password for login') + parser.add_argument('-U', '--linux-username', type=str, default='user1', + metavar='USER', help='Server-side Linux username') + parser.add_argument('-P', '--linux-password', type=str, default='1Password', + metavar='PASSWORD', help='Server-side Linux password') parser.add_argument('-a', '--all-nodes', action='store_true', help='Send command to all nodes reported by fabric') parser.add_argument('--threads', type=int, metavar='THREAD_COUNT', @@ -304,11 +308,6 @@ def build_parser(): help='discover server-side IP addresses') ipdiscover.add_argument('-A', '--aggressive', action='store_true', help='discover IPs aggressively') - ipdiscover.add_argument('-U', '--server-user', type=str, default='user1', - metavar='USER', help='Server-side Linux username') - ipdiscover.add_argument('-P', '--server-password', type=str, - default='1Password', metavar='PASSWORD', - help='Server-side Linux password') ipdiscover.add_argument('-6', '--ipv6', action='store_true', help='Discover IPv6 addresses') ipdiscover.add_argument('-I', '--interface', type=str, default=None, diff --git a/scripts/cxmux b/scripts/cxmux index 2b595eb..67f2d51 100755 --- a/scripts/cxmux +++ b/scripts/cxmux @@ -85,8 +85,9 @@ def main(): fabric = cxmanage_api.fabric.Fabric( ip_address=args.ecmeip, - username=args.user, - password=args.password + credentials={ + "ecme_username": args.user, "ecme_password": args.password + } ) ips = [node.ip_address for node in fabric.nodes.values()] if args.ssh: |