[PATCH] Added lnst-pool-wizard.
by Jiri Prochazka
More info on: https://github.com/jpirko/lnst/wiki/Pool-Wizard
Signed-off-by: Jiri Prochazka <jprochaz(a)redhat.com>
---
.gitignore | 1 +
lnst-pool-wizard | 67 +++++++++++
lnst/Controller/MessageDispatcherLite.py | 65 ++++++++++
lnst/Controller/Wizard.py | 199 +++++++++++++++++++++++++++++++
lnst/Slave/InterfaceManager.py | 2 +
lnst/Slave/NetTestSlave.py | 9 ++
6 files changed, 343 insertions(+)
create mode 100755 lnst-pool-wizard
create mode 100644 lnst/Controller/MessageDispatcherLite.py
create mode 100644 lnst/Controller/Wizard.py
diff --git a/.gitignore b/.gitignore
index 187b4b7..ec81cb1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
.#*
Logs/
+recipes/
# vim swap files
*.swp
diff --git a/lnst-pool-wizard b/lnst-pool-wizard
new file mode 100755
index 0000000..7853b3b
--- /dev/null
+++ b/lnst-pool-wizard
@@ -0,0 +1,67 @@
+#! /usr/bin/env python
+"""
+Machine pool wizard
+
+Copyright 2014 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+jprochaz(a)redhat.com (Jiri Prochazka)
+"""
+from lnst.Controller.Wizard import Wizard
+import sys
+import getopt
+
+RETVAL_PASS = 0
+RETVAL_FAIL = 1
+RETVAL_ERR = 2
+
+def help(retval = 0):
+ print "Usage:\n"\
+ " lnst-wizard [mode] [hostname[:port]]\n"\
+ "\n"\
+ "Modes:\n"\
+ " -h, --help display this help text and exit\n"\
+ " -i, --interactive start wizard in interactive mode (this "\
+ "is default mode)\n"\
+ " -n, --noninteractive start wizard in noninteractive mode\n"\
+ "Examples:\n"\
+ " lnst-wizard --interactive\n"\
+ " lnst-wizard --noninteractive 192.168.122.2\n"\
+ " lnst-wizard -n 192.168.122.2:8888 192.168.122.3:9999 192.168.122.4\n"
+ sys.exit(retval)
+
+
+def main():
+ try:
+ opts, args = getopt.getopt(
+ sys.argv[1:],
+ "hin",
+ ["help", "interactive", "noninteractive"]
+ )
+ except getopt.GetoptError as err:
+ print str(err)
+ help(RETVAL_ERR)
+ wizard = Wizard()
+ for opt, arg in opts:
+ if opt in ("-h", "--help"):
+ help()
+ elif opt in ("-i", "--interactive"):
+ wizard.interactive()
+ sys.exit(RETVAL_PASS)
+ elif opt in ("-n", "--noninteractive"):
+ if not args:
+ print "Nezadan zadny hostname!"
+ return RETVAL_ERR
+ wizard.noninteractive(args)
+ sys.exit(RETVAL_PASS)
+ else:
+ help(RET_ERR)
+ wizard.interactive()
+ sys.exit(RETVAL_PASS)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/lnst/Controller/MessageDispatcherLite.py b/lnst/Controller/MessageDispatcherLite.py
new file mode 100644
index 0000000..70a006a
--- /dev/null
+++ b/lnst/Controller/MessageDispatcherLite.py
@@ -0,0 +1,65 @@
+"""
+This module defines MessageDispatcherLite class which is derived from
+lnst.NetTestController.MessageDispatcher.
+
+Copyright 2011 Red Hat, Inc.
+Licensed under the GNU General Public License, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+jpirko(a)redhat.com (Jiri Pirko)
+"""
+
+from lnst.Common.ConnectionHandler import send_data, recv_data
+from lnst.Common.ConnectionHandler import ConnectionHandler
+
+class MessageDispatcherLite(ConnectionHandler):
+ def __init__(self):
+ super(MessageDispatcherLite, self).__init__()
+
+ def add_slave(self, machine, connection):
+ self.add_connection(1, connection)
+
+ def send_message(self, machine_id, data):
+ soc = self.get_connection(1)
+
+ if send_data(soc, data) == False:
+ msg = "Connection error from slave %s" % str(1)
+ raise NetTestError(msg)
+
+ def wait_for_result(self, machine_id):
+ wait = True
+ while wait:
+ connected_slaves = self._connections.keys()
+
+ messages = self.check_connections()
+
+ remaining_slaves = self._connections.keys()
+
+ for msg in messages:
+ if msg[1]["type"] == "result" and msg[0] == 1:
+ wait = False
+ result = msg[1]["result"]
+ else:
+ self._process_message(msg)
+
+ if connected_slaves != remaining_slaves:
+ disconnected_slaves = set(connected_slaves) -\
+ set(remaining_slaves)
+ msg = "Slaves " + str(list(disconnected_slaves)) + \
+ " disconnected from the controller."
+ raise NetTestError(msg)
+
+ return result
+
+ def _process_message(self, message):
+ if message[1]["type"] == "log":
+ pass
+ else:
+ msg = "Unknown message type: %s" % message[1]["type"]
+ raise NetTestError(msg)
+
+ def disconnect_slave(self, machine_id):
+ soc = self.get_connection(machine_id)
+ self.remove_connection(soc)
diff --git a/lnst/Controller/Wizard.py b/lnst/Controller/Wizard.py
new file mode 100644
index 0000000..d6c0031
--- /dev/null
+++ b/lnst/Controller/Wizard.py
@@ -0,0 +1,199 @@
+"""
+Wizard class for creating machine pool .xml files
+
+Copyright 2014 Red Hat Inc.
+Licensed under the GNU General Public Licence, version 2 as
+published by the Free Software Foundation; see COPYING for details.
+"""
+
+__author__ = """
+jprochaz(a)redhat.com (Jiri Prochazka)
+"""
+
+import socket
+import sys
+import os
+from lnst.Controller.Machine import Machine
+from lnst.Controller.MessageDispatcherLite import MessageDispatcherLite
+from lnst.Common.NetUtils import test_tcp_connection
+from lnst.Common.Utils import mkdir_p
+
+RETVAL_PASS = 0
+RETVAL_ERR = 1
+RPC_DEFAULT_PORT = 9999
+
+class Wizard:
+ def __init__(self):
+ self._msg_dispatcher = MessageDispatcherLite()
+ self._pool_dir = os.path.expanduser("~/.lnst/pool")
+ def interactive(self):
+ """
+ Starts Wizard in interactive mode. Wizard requests hostname and port
+ from user, tests connectivity to entered host, then he tries to connect
+ and configure slave on host machine, and finally he requests list of
+ ethernet interfaces in state DOWN. Then he writes them into .xml file
+ named by user. User can choose which interfaces should be added to the
+ .xml file.
+ """
+ while True:
+ pool_dir = raw_input("Enter path to pool directory "\
+ "(default: ~/.lnst/pool): ")
+ if pool_dir != "":
+ self._pool_dir = os.path.expanduser(pool_dir)
+ print "Pool directory set to %s" % self._pool_dir
+ while True:
+ hostname = raw_input("Enter hostname: ")
+ try:
+ # Tests if hostname is translatable into IP address
+ socket.gethostbyname(hostname)
+ break
+ except:
+ sys.stderr.write("Hostname not translatable into valid IP"\
+ " address.\n")
+ continue
+ while True:
+ port = raw_input("Enter port(default: 9999): ")
+ if port == "":
+ port = RPC_DEFAULT_PORT
+ try:
+ port = int(port)
+ break
+ except:
+ print "Invalid port."
+ continue
+ msg = self._get_suitable_interfaces(socket.gethostbyname(hostname),\
+ port)
+ if msg:
+ self._write_to_xml(msg, hostname, port, "interactive")
+ next_machine = raw_input("Do you want to add another machine? "\
+ "[Y/n]: ")
+ if next_machine.lower() == 'y' or next_machine == "":
+ continue
+ else:
+ break
+ return RETVAL_PASS
+
+ def noninteractive(self, hostlist):
+ """
+ Starts Wizard in noninteractive mode. Wizard gets list of hosts and
+ ports as arguments. He tries to connect and get info about suitable
+ interfaces for each of the hosts. Noninteractive mode does not prompt
+ user about anything, it automatically adds all suitable interfaces into
+ .xml file named the same as the hostname of the selected machine.
+ """
+ self._mode = "noninteractive"
+
+ for host in hostlist:
+ # Checks if port was entered along with hostname
+ if host.find(":") != -1:
+ hostname = host.split(':')[0]
+ try:
+ port = int(host.split(':')[1])
+ except:
+ port = RPC_DEFAULT_PORT
+ else:
+ hostname = host
+ port = RPC_DEFAULT_PORT
+ msg = self._get_suitable_interfaces(hostname, port)
+ if not msg:
+ continue
+ self._write_to_xml(msg, hostname, port, "noninteractive")
+
+ def _get_suitable_interfaces(self, hostname, port):
+ """
+ Calls all functions, which are used by both interactive and
+ noninteractive mode to get list of suitable interfaces. The list is
+ saved to variable msg.
+ """
+ if not test_tcp_connection(hostname, port):
+ sys.stderr.write("Host destination '%s:%s' unreachable.\n"
+ % (hostname, port))
+ return False
+ if not self._connect_and_configure_machine(hostname, port):
+ self._msg_dispatcher.disconnect_slave(1)
+ sys.stderr.write("Remote machine '%s:%s' configuration failed!\n"
+ % (hostname, port))
+ return False
+ msg = self._get_device_ifcs(hostname, port)
+ if not msg:
+ self._msg_dispatcher.disconnect_slave(1)
+ return False
+ self._msg_dispatcher.disconnect_slave(1)
+ return msg
+
+ def _get_device_ifcs(self, hostname, port):
+ """
+ Sends RPC call request to Slave to call function get_devices, which
+ returns list of ethernet devices which are in state DOWN.
+ """
+ msg = self._machine._rpc_call("get_devices")
+ if msg == {}:
+ sys.stderr.write("No suitable interfaces found on the slave "\
+ "'%s:%s'.\n" % (hostname, port))
+ return False
+ return msg
+
+ def _connect_and_configure_machine(self, hostname, port):
+ """
+ Connects to Slave and configures it
+ """
+ try:
+ self._machine = Machine(1, hostname, None, port)
+ self._machine.set_rpc(self._msg_dispatcher)
+ self._machine.configure("MachinePoolWizard")
+ return True
+ except:
+ return False
+
+ def _write_to_xml(self, msg, hostname, port, mode):
+ """
+ Used for writing desired output into .xml file. In interactive mode,
+ user is prompted for every interface, in noninteractive mode all
+ interfaces are automatically added to the .xml file.
+ """
+ if mode == "interactive":
+ output_file = raw_input("Enter the name of the output .xml file "\
+ "(without .xml, default is hostname.xml): ")
+ if mode == "noninteractive" or output_file == "":
+ output_file = hostname
+ xml_file = "<slavemachine>\n"\
+ " <params>\n"\
+ " <param name=\"hostname\" value=\"%s\"/>\n"\
+ " </params>\n"\
+ " <interfaces>\n" % hostname
+ devices_added = 0
+ for interface in msg.itervalues():
+ if mode == 'interactive':
+ answer = raw_input("Do you want to add interface '%s' (%s) "
+ "to the recipe? [Y/n]" % (interface['name'],
+ interface['hwaddr']))
+ if mode == "noninteractive" or answer.lower() == 'y'\
+ or answer == "":
+ devices_added += 1
+ xml_file += " <eth label=\"default_network\" "\
+ "id=\"%s\">\n"\
+ " <params>\n"\
+ " <param name=\"hwaddr\" "\
+ "value=\"%s\"/>\n"\
+ " </params>\n"\
+ " </eth>\n" % (interface['name'],
+ interface['hwaddr'])
+ xml_file += " </interfaces>\n"\
+ "</slavemachine>\n"
+ if devices_added == 0:
+ print "You didn't add any interface, no file '%s.xml' "\
+ "will be created!" % output_file
+ return RETVAL_ERR
+
+ mkdir_p(self._pool_dir)
+
+ try:
+ f = open(self._pool_dir + "/" + output_file + ".xml", 'w')
+ f.write(xml_file)
+ f.close()
+ except:
+ sys.stderr.write("File '%s.xml' could not be opened "\
+ "or data written." % output_file+"\n")
+ return RETVAL_ERR
+
+ print "File '%s.xml' successfuly created." % output_file
diff --git a/lnst/Slave/InterfaceManager.py b/lnst/Slave/InterfaceManager.py
index e30b52a..fc29af0 100644
--- a/lnst/Slave/InterfaceManager.py
+++ b/lnst/Slave/InterfaceManager.py
@@ -238,6 +238,7 @@ class Device(object):
self._conf = None
self._conf_dict = None
self._ip = None
+ self._ifi_type = None
self._state = None
self._master = {"primary": None, "other": []}
self._slaves = []
@@ -247,6 +248,7 @@ class Device(object):
def init_netlink(self, nl_msg):
self._if_index = nl_msg['index']
+ self._ifi_type = nl_msg['ifi_type']
self._hwaddr = normalize_hwaddr(nl_msg.get_attr("IFLA_ADDRESS"))
self._name = nl_msg.get_attr("IFLA_IFNAME")
self._state = nl_msg.get_attr("IFLA_OPERSTATE")
diff --git a/lnst/Slave/NetTestSlave.py b/lnst/Slave/NetTestSlave.py
index 0fbbcff..ec6dddb 100644
--- a/lnst/Slave/NetTestSlave.py
+++ b/lnst/Slave/NetTestSlave.py
@@ -119,6 +119,15 @@ class SlaveMethods:
return devices
+ def get_devices(self):
+ self._if_manager.rescan_devices()
+ devices = self._if_manager.get_devices()
+ result = {}
+ for device in devices:
+ if device._ifi_type == 1 and device._state == 'DOWN':
+ result[device._if_index] = {'name' : device._name, 'hwaddr' : device._hwaddr}
+ return result
+
def get_devices_by_devname(self, devname):
name_scan = self._if_manager.get_devices()
netdevs = []
--
1.9.3
9 years, 7 months
[PATCH] Daemon: use double fork for daemonization
by Jan Tluka
The daemon code used just a single fork to detach lnst-slave and become
a daemon. However this was insufficient and caused hangs when daemonized
inside Beaker jobs. The proper way is to use double fork method.
Signed-off-by: Jan Tluka <jtluka(a)redhat.com>
---
lnst/Common/Daemon.py | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/lnst/Common/Daemon.py b/lnst/Common/Daemon.py
index cf7dd22..2e13c72 100644
--- a/lnst/Common/Daemon.py
+++ b/lnst/Common/Daemon.py
@@ -64,6 +64,17 @@ class Daemon:
pid = os.fork()
if pid > 0:
sys.exit(0)
+ except OSError as e:
+ logging.error("fork failed: %d (%s)\n" % (e.errno, e.strerror))
+ sys.exit(1)
+
+ os.setsid()
+
+ # do the second fork
+ try:
+ pid = os.fork()
+ if pid > 0:
+ sys.exit(0)
pid = os.getpid()
self._write_pid(pid)
logging.info("deamonized with pid %d" % pid)
--
1.9.3
9 years, 7 months
[lnst] Daemon: use double fork for daemonization
by Jiří Pírko
commit c339114bb5d179d2fb71172fea35527e9091c1bb
Author: Jan Tluka <jtluka(a)redhat.com>
Date: Fri Sep 19 10:45:56 2014 +0200
Daemon: use double fork for daemonization
The daemon code used just a single fork to detach lnst-slave and become
a daemon. However this was insufficient and caused hangs when daemonized
inside Beaker jobs. The proper way is to use double fork method.
Version 2:
Added missing file descriptor redirection.
Signed-off-by: Jan Tluka <jtluka(a)redhat.com>
Signed-off-by: Jiri Pirko <jiri(a)resnulli.us>
lnst/Common/Daemon.py | 22 ++++++++++++++++++++++
1 files changed, 22 insertions(+), 0 deletions(-)
---
diff --git a/lnst/Common/Daemon.py b/lnst/Common/Daemon.py
index cf7dd22..9dd2847 100644
--- a/lnst/Common/Daemon.py
+++ b/lnst/Common/Daemon.py
@@ -64,9 +64,31 @@ class Daemon:
pid = os.fork()
if pid > 0:
sys.exit(0)
+ except OSError as e:
+ logging.error("fork failed: %d (%s)\n" % (e.errno, e.strerror))
+ sys.exit(1)
+
+ os.setsid()
+
+ # do the second fork
+ try:
+ pid = os.fork()
+ if pid > 0:
+ sys.exit(0)
pid = os.getpid()
self._write_pid(pid)
logging.info("deamonized with pid %d" % pid)
except OSError as e:
logging.error("fork failed: %d (%s)\n" % (e.errno, e.strerror))
sys.exit(1)
+
+ # redirect the file descs
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+ si = file("/dev/null", 'r')
+ so = file("/dev/null", 'a+')
+ se = file("/dev/null", 'a+', 0)
+ os.dup2(si.fileno(), sys.stdin.fileno())
+ os.dup2(so.fileno(), sys.stdout.fileno())
+ os.dup2(se.fileno(), sys.stderr.fileno())
9 years, 7 months
[PATCH] IcmpPing: add size option
by Jan Tluka
Signed-off-by: Jan Tluka <jtluka(a)redhat.com>
---
test_modules/IcmpPing.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/test_modules/IcmpPing.py b/test_modules/IcmpPing.py
index 5dfc5b2..5b9d888 100644
--- a/test_modules/IcmpPing.py
+++ b/test_modules/IcmpPing.py
@@ -28,6 +28,9 @@ class IcmpPing(TestGeneric):
iface = self.get_opt("iface")
if iface:
cmd += " -I %s" % iface
+ size = self.get_opt("size")
+ if size:
+ cmd += " -s %s" % size
return cmd
def run(self):
--
1.9.3
9 years, 7 months
[lnst] IcmpPing: add size option
by Jiří Pírko
commit 7e498f8f0d2dc309093f4458a945e19a340dcde2
Author: Jan Tluka <jtluka(a)redhat.com>
Date: Tue Sep 16 10:50:14 2014 +0200
IcmpPing: add size option
Signed-off-by: Jan Tluka <jtluka(a)redhat.com>
Signed-off-by: Jiri Pirko <jiri(a)resnulli.us>
test_modules/IcmpPing.py | 3 +++
1 files changed, 3 insertions(+), 0 deletions(-)
---
diff --git a/test_modules/IcmpPing.py b/test_modules/IcmpPing.py
index 5dfc5b2..5b9d888 100644
--- a/test_modules/IcmpPing.py
+++ b/test_modules/IcmpPing.py
@@ -28,6 +28,9 @@ class IcmpPing(TestGeneric):
iface = self.get_opt("iface")
if iface:
cmd += " -I %s" % iface
+ size = self.get_opt("size")
+ if size:
+ cmd += " -s %s" % size
return cmd
def run(self):
9 years, 7 months