diff options
author | apw <apw@592f7852-d20e-0410-864c-8624ca9c26a4> | 2006-12-07 21:01:14 +0000 |
---|---|---|
committer | apw <apw@592f7852-d20e-0410-864c-8624ca9c26a4> | 2006-12-07 21:01:14 +0000 |
commit | 4c6ec3807c2b038e0d762041b745124a831f9cfa (patch) | |
tree | 0cfbcb0ce9dfe3fecfde5cc3397faf5ae919d0d9 /conmux | |
parent | a43eb8a274e0da9f48b6d4914c5cb72ca924d354 (diff) |
conmux: initial import of the Console Multiplexor
Import the Console Multiplexor (CONMUX) subsystem, developed by
the IBM Linux Technology Center. CONMUX is a console abstractor,
presenting any console with a consistent location, naming and
semantic. Access to the console and hardreset of the machine is
the same regardless of the underlying access methodology.
Through this abstraction we create a simple and consistent interface
to disparate consoles simplifying programatic use of the console.
This provides for easy integration of console handling into a
test harness. This allow CONMUX to be used to capture Linux kernel
messages only available on the external system console, as well
as providing a framework for detecting, diagnosing and rescuing
paniced and hung systems.
Signed-off-by: Andy Whitcroft <andyw@uk.ibm.com>
Acked-by: Dustin Kirkland <dustin.kirkland@us.ibm.com>
git-svn-id: svn://test.kernel.org/autotest/trunk@425 592f7852-d20e-0410-864c-8624ca9c26a4
Diffstat (limited to 'conmux')
-rw-r--r-- | conmux/.version | 1 | ||||
-rw-r--r-- | conmux/COPYING | 348 | ||||
-rw-r--r-- | conmux/Conmux.pm | 209 | ||||
-rwxr-xr-x | conmux/INSTALL | 34 | ||||
-rw-r--r-- | conmux/Makefile | 47 | ||||
-rw-r--r-- | conmux/README | 61 | ||||
-rwxr-xr-x | conmux/conmux | 1289 | ||||
-rwxr-xr-x | conmux/conmux-attach | 88 | ||||
-rwxr-xr-x | conmux/conmux-registry | 282 | ||||
-rw-r--r-- | conmux/conmux.html | 158 | ||||
-rwxr-xr-x | conmux/console | 192 | ||||
-rwxr-xr-x | conmux/drivers/blade | 234 | ||||
-rwxr-xr-x | conmux/drivers/hmc | 597 | ||||
-rwxr-xr-x | conmux/drivers/module.mk | 15 | ||||
-rwxr-xr-x | conmux/drivers/reboot-netfinity | 98 | ||||
-rwxr-xr-x | conmux/drivers/reboot-newisys | 49 | ||||
-rwxr-xr-x | conmux/drivers/reboot-numaq | 224 | ||||
-rwxr-xr-x | conmux/drivers/reboot-rsa | 101 | ||||
-rwxr-xr-x | conmux/drivers/reboot-rsa2 | 91 | ||||
-rwxr-xr-x | conmux/examples/README | 11 | ||||
-rwxr-xr-x | conmux/examples/command.cf | 4 | ||||
-rwxr-xr-x | conmux/examples/socket.cf | 4 | ||||
-rwxr-xr-x | conmux/helpers/autoboot-helper | 72 | ||||
-rwxr-xr-x | conmux/helpers/module.mk | 14 | ||||
-rwxr-xr-x | conmux/helpers/tickle-helper | 99 | ||||
-rwxr-xr-x | conmux/mkdeb | 9 | ||||
-rwxr-xr-x | conmux/start | 138 |
27 files changed, 4469 insertions, 0 deletions
diff --git a/conmux/.version b/conmux/.version new file mode 100644 index 00000000..e906e569 --- /dev/null +++ b/conmux/.version @@ -0,0 +1 @@ +conmux 1 00 00 1000 diff --git a/conmux/COPYING b/conmux/COPYING new file mode 100644 index 00000000..80e35181 --- /dev/null +++ b/conmux/COPYING @@ -0,0 +1,348 @@ +The Console Multiplexor + +(C) Copyright IBM Corp. 2004, 2005, 2006 + +The Console Multiplexor is released under the GNU Public Licence V2 +a copy of which should be included below. + + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/conmux/Conmux.pm b/conmux/Conmux.pm new file mode 100644 index 00000000..47b8cd27 --- /dev/null +++ b/conmux/Conmux.pm @@ -0,0 +1,209 @@ +# +# Conmux.pm -- core console multiplexor package +# +# Implements the core multiplexor functionality such as resolution of +# names and connecting to the conmux server. +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +package Conmux; +use URI::Escape; + +our $Config; + +BEGIN { + my $cf = '/usr/local/conmux/etc/config'; + if (-f $cf) { + open(CFG, "<$cf") || die "Conmux: $cf: open failed - $!\n"; + while(<CFG>) { + chomp; + next if (/^#/ || /^\s*$/ || !/=/); + + my ($name, $value) = split(/=/, $_, 2); + $value =~ s/^"//; + $value =~ s/"$//; + + # Substitute variables. + while ($value =~ /\$([A-Za-z0-9_]+)/) { + my $v = $Config->{$1}; + $value =~ s/\$$1/$v/; + } + $Config->{$name} = $value; + } + close(CFG); + } +} + +sub encodeArgs { + my (%a) = @_; + my ($a, $n, $s); + + ##print "0<$_[0]> ref<" . ref($_[0]) . ">\n"; + + # Handle being passed references to hashes too ... + $a = \%a; + $a = $_[0] if (ref($_[0]) eq "HASH"); + + for $n (sort keys %{$a}) { + $s .= uri_escape($n) . '=' . uri_escape($a->{$n}) . + ' '; + } + chop($s); + $s; +} + +sub decodeArgs { + my ($s) = @_; + my (%a, $nv, $n, $v); + + # Decode the standard argument stream. + for $nv (split(' ', $s)) { + ($n, $v) = split('=', $nv, 2); + $a{uri_unescape($n)} = uri_unescape($v); + } + + %a; +} + +sub sendCmd { + my ($fh, $c, $a) = @_; + my ($rs); + + # Send the encoded command ... + print $fh $c . " " . encodeArgs($a) . "\n"; + + # Read the reply. + $rs = <$fh>; + chomp($rs); + + decodeArgs($rs); +} + +sub sendRequest { + my ($fh, $c, $a) = @_; + my %a = { 'result' => 'more' }; + + # Send the encoded command ... + print $fh $c . " " . encodeArgs($a) . "\n"; + + %a; +} +sub revcResult { + my ($fh) = @_; + my ($rs); + + # Read the reply. + $rs = <$fh>; + chomp($rs); + + decodeArgs($rs); +} + +# +# Configuration. +# +sub configRegistry { + my $reg = $Config->{'registry'}; + + $reg = "localhost" if (!$reg); + $reg; +} + +# Connect to the host/port specified on the command line, +# or localhost:23 +sub connect { + my ($to) = @_; + my ($reg, $sock); + + # host:port + if ($to =~ /:/) { + # Already in the right form. + + # registry/service + } elsif ($to =~ m@(.*)/(.*)@) { + my ($host, $service) = ($1, $2); + + $to = Conmux::Registry::lookup($host, $service); + + # service + } else { + $to = Conmux::Registry::lookup('-', $to); + } + + $sock = new IO::Socket::INET(Proto => 'tcp', PeerAddr => $to) + or die "Conmux::connect $to: connect failed - $@\n"; + + # Turn on keep alives by default. + $sock->sockopt(SO_KEEPALIVE, 1); + + $sock; +} + +package Conmux::Registry; +sub lookup { + my ($host, $service) = @_; + + $host = Conmux::configRegistry() if ($host eq '-'); + + # Connect to the registry service and lookup the requested service. + my $reg = new IO::Socket::INET(Proto => 'tcp', + PeerAddr => "$host", PeerPort => 63000) + or die "Conmux::connect: registry not available - $@\n"; + + my %r = Conmux::sendCmd($reg, 'LOOKUP', { 'service' => $service }); + die "Conmux::Registry::lookup: $service: error - $r{'status'}\n" + if ($r{status} ne "OK"); + + close($reg); + + $r{'result'}; +} + +sub add { + my ($host, $service, $location) = @_; + + $host = Conmux::configRegistry() if ($host eq '-'); + + # Connect to the registry service and lookup the requested service. + my $reg = new IO::Socket::INET(Proto => 'tcp', + PeerAddr => "$host", PeerPort => 63000) + or die "Conmux::connect: registry not available - $@\n"; + + my %r = Conmux::sendCmd($reg, 'ADD', { 'service' => $service, + 'location' => $location }); + die "Conmux::Registry::add: $service: error - $r{'status'}\n" + if ($r{status} ne "OK"); + + close($reg); + + 1; +} + +sub list { + my ($host, $service, $location) = @_; + my (@results, %r); + + $host = Conmux::configRegistry() if ($host eq '-'); + + # Connect to the registry service and ask for a list. + my $reg = new IO::Socket::INET(Proto => 'tcp', + PeerAddr => "$host", PeerPort => 63000) + or die "Conmux::connect: registry not available - $@\n"; + + %r = Conmux::sendCmd($reg, 'LIST', { }); +## while ($r{'status'} eq 'more') { +## %r = receiveResult($reg); +## push(@results, $r{'result'}); +## } + die "Conmux::Registry::list: error - $r{'status'}\n" + if ($r{'status'} ne "OK"); + + close($reg); + + $r{'result'}; +} + +1; diff --git a/conmux/INSTALL b/conmux/INSTALL new file mode 100755 index 00000000..24f1de35 --- /dev/null +++ b/conmux/INSTALL @@ -0,0 +1,34 @@ +Prerequisites +============= +This package required the following perl modules to be installed: + o IO::Multiplex; + +Debian Packages: + o libio-multiplex-perl + +Fedora Packages: + o perl-IO-Multiplex + + +Building +======== +To make and install this package to the default location +(/usr/local/conmux): + + # make install + +To an alternative location: + + # make PREFIX=/usr/alt/conmux install + +To build for a specified prefix, but installed into a temporary tree: + + # make PREFIX=/usr/alt/conmux BUILD=build/location install + + +Legal +===== +(C) Copyright IBM Corp. 2004, 2005, 2006 +Author: Andy Whitcroft <andyw@uk.ibm.com> + +The Console Multiplexor is released under the GNU Public License V2 diff --git a/conmux/Makefile b/conmux/Makefile new file mode 100644 index 00000000..f78d205d --- /dev/null +++ b/conmux/Makefile @@ -0,0 +1,47 @@ +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +BUILD= +PREFIX=/usr/local/conmux +BASE=$(BUILD)$(PREFIX) +BINS=console conmux-attach +LIBS=Conmux.pm +SBIN=conmux-registry conmux start + +MODULES=helpers drivers + +all:: + +install:: + @[ -d $(BASE) ] || mkdir -p $(BASE) + @[ -d $(BASE)/bin ] || mkdir $(BASE)/bin + @[ -d $(BASE)/lib ] || mkdir $(BASE)/lib + @[ -d $(BASE)/sbin ] || mkdir $(BASE)/sbin + @[ -d $(BASE)/log ] || mkdir $(BASE)/log + @[ -d $(BASE)/etc ] || mkdir $(BASE)/etc + for f in $(BINS); do \ + rm -f $(BASE)/bin/$$f; \ + cp -p $$f $(BASE)/bin/$$f; \ + chmod 755 $(BASE)/bin/$$f; \ + done + for f in $(SBIN); do \ + rm -f $(BASE)/sbin/$$f; \ + cp -p $$f $(BASE)/sbin/$$f; \ + chmod 755 $(BASE)/sbin/$$f; \ + done + for f in $(LIBS); do \ + rm -f $(BASE)/lib/$$f; \ + cp -p $$f $(BASE)/lib/$$f; \ + chmod 644 $(BASE)/lib/$$f; \ + done + +release:: + $(MAKE) BUILD=build install + (cd build; tar cf - *) | gzip >conmux.tgz + rm -rf build + +clean:: + rm -f conmux.tgz + +include $(patsubst %, %/module.mk, $(MODULES)) diff --git a/conmux/README b/conmux/README new file mode 100644 index 00000000..d9d34c4c --- /dev/null +++ b/conmux/README @@ -0,0 +1,61 @@ +CONMUX -- Console Multiplexor +============================= +CONMUX is a console abstractor. Presenting any console with a +consistent location, naming and semantic. Access to the console, +and hardreset of the machine is the same regardless of the underlying +access methodology. + + +License +------- +See the COPYING file for details. + + +Installation +------------ +See the INSTALL file for details. + + +Configuration +------------- +A console multiplexor is defined using a per instance configuration file. +The standard startup scripts expect this to be in the ROOT/etc and have +a .cf suffix. + +Below is an example configuration defining the console for a NUMA-Q: + + $ cat kite.cf + # START:autoboot + listener kite + socket console 'kite console' console.server.here.com:6000 + command 'hardreset' 'initated a hard reset' \ + 'conmux-attach -o status localhost/kite reboot-numaq vcs \ + vcsconsole.server.here.com kite 12346 administrator passwd' + $ + +These are connected to using the console command, using the symbolic console +name: + + $ console kite + + +Automatic Startup +----------------- +See the start script for automated startup of console daemons. +This script is designed to be used at system boot time out of +/etc/init.d. + + +Documentation and Examples +-------------------------- +More detailed information on the workings of conmux can be found in +'conmux.html'. Example configuration files can be found in the +'examples' directory. + + +Legal +----- +(C) Copyright IBM Corp. 2004, 2005, 2006 +Author: Andy Whitcroft <andyw@uk.ibm.com> + +The Console Multiplexor is released under the GNU Public License V2 diff --git a/conmux/conmux b/conmux/conmux new file mode 100755 index 00000000..6f628433 --- /dev/null +++ b/conmux/conmux @@ -0,0 +1,1289 @@ +#!/usr/bin/perl +# +# conmux -- the main console multiplexor daemon +# +# Main console multiplexor daemon. There is one of these daemons for +# each open console supported in the system. Clients are directed to +# this daemon via the conmux-registry deamon. +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +use strict; + +use Symbol qw(gensym); + +use IO::Socket; +use IO::Multiplex; +use IPC::Open3; +use URI::Escape; +use Net::Domain; + +# Find our internal libraries. +###my $lib = $0; $lib =~ s@/[^/]+$@@; +###push(@INC, $lib); +###require Conmux; +##use lib "."; +##use Conmux; +use lib "/usr/local/conmux/lib"; +use Conmux; + +our $P = 'conmux'; +our $debug = 0; + +$SIG{'CHLD'} = "IGNORE"; + +$| = 1; + +# +# CALLBACK: this class is used to provide a timed callback. The multiplexor +# libarary allows us to set a timeout on any open file we have registered. +# So, we open a new file descriptor to /dev/null and set a timeout on that. +# +package Callback; + +sub new { + my ($class, $mux, $who, $time) = @_; + my $self = bless { 'who' => $who }, $class; + + my ($fh); + + print "Callback::new [$self] mux<$mux> who<$who> time<$time>\n" + if ($main::debug); + + # Open a file handle to nothing, we need this to hang the timeout + # on in the multiplexor. It will fail with a mux_eof, which we ignore. + open($fh, "</dev/null") || die "$P: /dev/null: open failed - $!\n"; + + $mux->add($fh); + $mux->set_callback_object($self, $fh); + + $mux->set_timeout($fh, $time); + + $self; +} +sub mux_timeout { + my ($self, $mux, $fh) = @_; + + print "Callback::mux_timeout [$self] mux<$mux> fh<$fh>\n" + if ($main::debug); + + $self->{'who'}->callback_timeout(); + + $mux->close($fh); +} +sub DESTROY { + my ($self) = @_; + print "Callback::DESTROY [$self]\n" if ($main::debug); +} + +# +# LISTENER SOCKET: creates an intenet listener for new clients and +# connects them to the junction provided. +# +package ListenerSocket; + +sub new { + my ($class, $mux, $port) = @_; + my $self = bless { 'mux' => $mux }, $class; + + print "ListenerSocket::new [$self] mux<$mux> port<$port>\n" + if ($main::debug); + + $self->initialise($port); + + $self; +} + +sub initialise { + my ($self, $port) = @_; + my ($sock); + + print "ListenerSocket::initialise [$self] port<$port> " + if ($main::debug); + + # Create a listening socket and add it to the multiplexor. + my $sock = new IO::Socket::INET(Proto => 'tcp', + LocalPort => $port, + Listen => 4, + ReuseAddr => 1) + or die "socket: $@"; + + print " adding $self $sock\n" if ($main::debug); + $self->mux->listen($sock); + $self->mux->set_callback_object($self, $sock); + $self->listener($sock); +} + +# DATA accessors. +sub mux { + my $self = shift; + if (@_) { $self->{'mux'} = shift } + return $self->{'mux'}; +} +sub listener { + my $self = shift; + if (@_) { $self->{'listener'} = shift } + return $self->{'listener'}; +} + +sub address { + my ($self) = @_; + Net::Domain::hostfqdn() . ':' . $self->{'listener'}->sockport(); +} + +# JUNCTION: callbacks. +##sub junctionInput { +##} +##sub junctionEOF { +## my ($self) = @_; +## +## $self->{'junction'}->junctionRemove($self, 'console-client'); +## $self->{'mux'}->close($self->{'listener'}); +##} + +# Handle new connections by instantiating a new client class. +sub mux_connection { + my ($self, $mux, $fh) = @_; + my ($client); + + print "ListenerSocket::mux_connection [$self] mux<$mux> fh<$fh>\n" + if ($main::debug); + + # Make a new client connection. + $client = ClientCmd->new($mux, $fh); + print " new connection $self $client\n" if ($main::debug); +} + +sub DESTROY { + my ($self) = @_; + + print "ListenerSocket::DESTROY [$self]\n" if ($main::debug); + + close($self->listener); +} + +# +# JUNCTION: generic junction box object, connects names groups of objects +# to other named groups. +# +# Expects the following callbacks to be defined on each object registered: +# junctionInput($from, $data) +# junctionEOF($from, $to) +# +package Junction; + +sub new { + my ($class) = @_; + my $self = bless { }, $class; + + print "Junction::new [$self]\n" if ($main::debug); + + $self; +} + +sub junctionAdd { + my ($self, $client) = @_; + + print "Junction::junctionAdd [$self] client<$client>\n" + if ($main::debug); + + # Add ourselves to the list of recipients. + $self->{$client} = $client; +} + +sub junctionInput { + my ($self, $client, $data) = @_; + my ($c); + + print "Junction::junctionInput [$self] client<$client> " . + "data<$data>\n" if ($main::debug); + + # Send this data on to the clients listed in the output list. + for $c (values %{$self}) { + print " sending to $c\n" if ($main::debug); + $c->junctionInput($client, $data); + } +} +sub junctionEOF { + my ($self, $client) = @_; + my ($c); + + print "Junction::junctionEOF [$self] client<$client>\n" + if ($main::debug); + + # Send this eof on to the clients listed in the output list. + for $c (values %{$self}) { + print " sending to $c\n" if ($main::debug); + $c->junctionEOF($client); + } +} +sub junctionRemove { + my ($self, $client) = @_; + + print "Junction::junctionRemove [$self] client<$client>\n" + if ($main::debug); + + # Drop this client from our lists. + delete $self->{$client}; +} + +# +# PAYLOAD: generic payload object, connects itself to the requisite junction. +# +package Payload; + +my %payloads = (); +my $payloads = 0; + +sub lookup { + my ($class, $name) = @_; + + $payloads{$name}; +} + +sub found { + my ($class, $name, $self) = @_; + + print "Payloads::found name<$name> self<$self>\n" if ($main::debug); + + $payloads{$name} = $self; + $payloads++; +} +sub lost { + my ($class, $name, $self) = @_; + + print "Payloads::lost name<$name> self<$self>\n" if ($main::debug); + + undef $payloads{$name}; + if (--$payloads == 0) { + exit(0); + } +} + +sub new { + my ($class, $name, $title, $mux, @a) = @_; + my $self = bless { }, $class; + + print "Payload::new [$self] name<$name> title<$title> mux<$mux>\n" + if ($main::debug); + + Payload->found($name, $self); + + $self->name($name); + $self->title($title); + $self->mux($mux); + $self->enabled(1); + + $self->cin(Junction->new); + $self->cout(Junction->new); + + $self->initialise(@a); + + $self; +} + +# Data accessors. +sub name { + my $self = shift; + if (@_) { $self->{'name'} = shift } + return $self->{'name'}; +} +sub title { + my $self = shift; + if (@_) { $self->{'title'} = shift } + return $self->{'title'}; +} +sub mux { + my $self = shift; + if (@_) { $self->{'mux'} = shift } + return $self->{'mux'}; +} +sub cin { + my $self = shift; + if (@_) { $self->{'cin'} = shift } + return $self->{'cin'}; +} +sub cout { + my $self = shift; + if (@_) { $self->{'cout'} = shift } + return $self->{'cout'}; +} +sub enabled { + my $self = shift; + if (@_) { $self->{'enabled'} = shift } + return $self->{'enabled'}; +} +sub connected { + my $self = shift; + if (@_) { $self->{'connected'} = shift } + $self->transition(); + return $self->{'connected'}; +} +sub transition { + my $self = shift; + my $time = time; + if (($time - $self->{'trans_minor'}) > 30) { + $self->{'trans_major'} = $time; + } + $self->{'trans_minor'} = $time; +} +sub retry_timeout { + my $self = shift; + my $time = time - $self->{'trans_major'}; + + if ($time < 60) { + return 1; + } elsif ($time < 120) { + return 10; + } else { + return 30; + } +} +sub state { + my $self = shift; + my $ctime = $self->{'connected'}; + my $ttime = $self->{'trans_major'}; + my $time = time; + + if ($ctime && ($time - $ctime) > 30) { + "connected"; + } elsif ($ttime && ($time - $ttime) < 60) { + "transition"; + } else { + "disconnected"; + } +} + +sub initialise { + my ($self) = @_; + my ($sock); + + print "Payload::initialise [$self]\n" if ($main::debug); + + # Ensure we recieve client input. + $self->cin->junctionAdd($self); + + $self->connected(time); +} + +# Telnet constants. +my $TN_IAC = sprintf("%c", 255); +my $TN_DONT = sprintf("%c", 254); +my $TN_DO = sprintf("%c", 253); +my $TN_WONT = sprintf("%c", 252); +my $TN_WILL = sprintf("%c", 251); +my $TN_SB = sprintf("%c", 250); +my $TN_SE = sprintf("%c", 240); +my $TN_BREAK = sprintf("%c", 243); + +my $TNOPT_ECHO = sprintf("%c", 1); +my $TNOPT_SGA = sprintf("%c", 3); + +# +# If we get here then we have accumulated a complete telnet +# negotiation string. +# +# Telnet negotiation protocol - RFC#854: +# +# DO We are being asked to DO an option +# DONT We are being asked to NOT DO an option +# WILL We are being told they will DO an option +# WONT We are being told they will NOT DO an option +# +# DO/DONT requests indicate we should {en,dis}able a mode. +# We are expected to respond with WILL or WONT. To prevent +# loops, we should not respond if the request matches our +# current mode. +# +# WILL/WONT requests indicate the other end would like to +# {en,dis}able a mode. We are expected to respond with +# DO/DONT. +# +# If we want a particular mode {en,dis}abled then we may start +# negotiation of that mode with a WILL/WONT. +# +# We want the other end to perform echo by default so we will +# DO any request for ECHO and DONT all other requests. +# + +sub mux_input { + my ($self, $mux, $fh, $input) = @_; + my ($client); + + print "Payload::mux_input [$self] mux<$mux> fh<$fh> input<$$input>\n" + if ($main::debug); + + while ($$input ne "") { + # Ordinary text. + if ($$input =~ s/^([^$TN_IAC]+)//) { + # Data coming in from the payload, this needs to go to + # all of the clients. + $self->cout->junctionInput($self, $1); + next; + } + + # IAC,SB,...,SE + if ($$input =~ s/^$TN_IAC$TN_SB([^$TN_SE]+)$TN_SE//) { + print "SB\n" if ($main::debug); + next; + } + # IAC,[DO|DONT|WILL|WONT],<what> + if ($$input =~ s/^$TN_IAC$TN_DO(.)//) { + my $c = unpack("C", $1); + print "DO<$c:$1>\n" if ($main::debug); + # We are DONT on all options so WONT all requests. + $self->junctionInput($self, "$TN_IAC$TN_WONT$1"); + next; + } + if ($$input =~ s/^$TN_IAC$TN_DONT(.)//) { + my $c = unpack("C", $1); + print "DONT<$c:$1>\n" if ($main::debug); + # We are already DONT on all options, no reply. + next; + } + if ($$input =~ s/^$TN_IAC$TN_WILL(.)//) { + my $c = unpack("C", $1); + print "WILL<$c:$1>\n" if ($main::debug); + + my $reply = $TN_DONT; + if ($1 == $TNOPT_ECHO || $1 == $TNOPT_SGA) { + $reply = $TN_DO; + } + $self->junctionInput($self, "$TN_IAC$reply$1"); + next; + } + if ($$input =~ s/^$TN_IAC$TN_WONT(.)//) { + my $c = unpack("C", $1); + print "WONT<$c:$1>\n" if ($main::debug); + $self->junctionInput($self, "$TN_IAC$TN_DONT$1"); + next; + } + # IAC,<option> + if ($$input =~ s/^$TN_IAC([^$TN_SB$TN_DO$TN_DONT$TN_WILL$TN_WONT])//) { + print "OPTION<$1>\n" if ($main::debug); + next; + } + + # Incomplete ... + if ($$input =~ /^$TN_IAC/) { + return; + } + } +} +sub junctionInput { + my ($self, $from, $data) = @_; + my ($fh); + + print "Payload::junctionInput [$self] from<$from> data<$data>\n" + if ($main::debug); + + ##$self->{'mux'}->write($self->{'wfh'}, $data); + # If we are connected ... + if ($self->{'wfh'}) { + $fh = $self->{'wfh'}; + print $fh $data; + } else { + $from->junctionInput($self, "<<<NOT CONNECTED>>>\n"); + } +} + +sub mux_eof { + my ($self, $mux, $fh) = @_; + my ($client); + + print "Payload::mux_eof [$self] mux<$mux> fh<$fh>\n" if ($main::debug); + + # Check for a restartable connection. + if ($self->can("restart")) { + my ($timeout) = $self->retry_timeout(); + + $self->cout->junctionInput($self, + "<<<PAYLOAD LOST ... retrying in $timeout secs>>>\n"); + + # Schedule a timeout to trigger a reconnect. + Callback->new($mux, $self, $timeout); + + } else { + $self->cout->junctionEOF($self); + $self->cin->junctionRemove($self); + + Payload->lost($self->name, $self); + } + + # Close down the payload ... + $mux->close($self->{'rfh'}); + ##$mux->remove($self->{'wfh'}); +} + +sub mux_close { + my ($self, $mux, $fh) = @_; + + $self->connected(0); + + #close($self->{'rfh'}); + close($self->{'wfh'}); + undef $self->{'rfh'}; + undef $self->{'wfh'}; + + if ($self->{'pid'}) { + # Kill the process group for this pid. + kill 1, 0 - $self->{'pid'}; + undef $self->{'pid'}; + } +} + +sub callback_timeout { + my ($self) = @_; + + print "Payload::callback_timeout [$self]\n" if ($main::debug); + + if ($self->enabled) { + $self->cout->junctionInput($self, "<<<PAYLOAD RESTART>>>\n"); + $self->openPayload(); + } else { + $self->cout->junctionInput($self, "<<<PAYLOAD DISABLED>>>\n"); + } +} + +sub closePayload { + my ($self) = @_; + + if ($self->connected) { + $self->cout->junctionInput($self, "<<<PAYLOAD CLOSED>>>\n"); + + # Close down the payload ... + $self->mux->close($self->{'rfh'}); + } + if ($self->enabled) { + $self->enabled(0); + return 1; + } else { + return 0; + } +} +sub openPayload { + my ($self) = @_; + + $self->enabled(1); + if (!$self->connected) { + if ($self->can("restart")) { + $self->restart(); + + return 1; + } + } + return 0; +} + +sub helpAdd { + my ($self, $cmd, $msg) = @_; + + push(@{$self->{'help'}}, [ $cmd, $msg ]); +} + +sub commandHelp { + my ($self) = @_; + my @entries = ( + [ 'break', 'send a break sequence' ] + ); + + if (defined $self->{'help'}) { + ( @entries, @{$self->{'help'}} ); + + } else { + @entries; + } +} +sub commandAdd { + my ($self, $cmd, @a) = @_; + + $self->{'cmd'}->{$cmd} = [ @a ]; +} +sub commandExec { + my ($self, $client, $cmd, $a) = @_; + my ($exe); + + print "Payload::commandExec [$self] client<$client> cmd<$cmd> a<$a>\n" + if ($main::debug); + + $exe = $self->{'cmd'}->{$cmd}; + + if ($cmd eq "break") { + # Send a telnet break ... + $self->junctionInput($self, "$TN_IAC$TN_BREAK"); + return; + + } elsif ($cmd eq "close") { + if (!$self->enabled && !$self->connected) { + $client->junctionInput($self, + "console already closed\n"); + + } elsif ($self->closePayload()) { + $self->cout->junctionInput($self, "(" . $client->id . + ") triggered a console close\n"); + + } else { + $client->junctionInput($self, "ERROR: close failed\n"); + } + return; + + } elsif ($cmd eq "open") { + if ($self->connected) { + $client->junctionInput($self, "console already open\n"); + + } elsif ($self->openPayload()) { + $self->cout->junctionInput($self, "(" . $client->id . + ") triggered a console open\n"); + + } else { + $client->junctionInput($self, "open failed\n"); + } + return; + } + + # Ensure we error if we have no command. + if (!$exe) { + $client->junctionInput($self, "Command not recognised\n"); + return; + } + + my ($msg, $run) = @{$exe}; + if ($msg ne '') { + $self->cout->junctionInput($self, "(" . $client->id . + ") $msg\n"); + } + + local(*IN, *OUT, *ERR); + my $pid = IPC::Open3::open3(*IN, *OUT, *ERR, $run); + close(*IN{IO}); + + # XXX: this should not be happening here. + $self->mux->add(*OUT{IO}); + my $data = ClientData->new($self->mux, *OUT{IO}); + $data->{'id'} = "cmd:$cmd stdout"; + + $data->payload($self); + $data->cout($self->cout); + + # XXX: this should not be happening here. + $self->mux->add(*ERR{IO}); + my $data = ClientData->new($self->mux, *ERR{IO}); + $data->{'id'} = "cmd:$cmd stderr"; + + $data->payload($self); + $data->cout($client); +} + +sub DESTROY { + my ($self) = @_; + + print "Payload::DESTROY [$self]\n" if ($main::debug); +} + +# +# PAYLOAD APPLICATION: handles forking off a command as a payload. +# +package PayloadApplication; +use base 'Payload'; + +sub initialise { + my ($self, $cmd) = @_; + my ($pid, $wfh, $rfh); + + print "PayloadApplication::initialise [$self] cmd<$cmd>" + if ($main::debug); + + $self->SUPER::initialise(); + + # XXX: we cannot use the write buffering offered by the mux package + # without suffering a read error from the PWR file handle, there + # is no a way to add a write-only channel. + + $self->{'args'} = $cmd; + + # Start the payload ... + $pid = IPC::Open3::open3($wfh, $rfh, 0, "setsid " . $cmd); + + $self->{'rfh'} = $rfh; + $self->{'wfh'} = $wfh; + $self->{'pid'} = $pid; + + $self->mux->add($rfh); + ##$mux->add($wfh); + + $self->mux->set_callback_object($self, $rfh); + ##$mux->set_callback_object($self, $wfh); + + print "SHARE PAYLOAD: $self $wfh/$rfh (to $cmd) [fd=" . + fileno($wfh) . "/" . fileno($rfh) . "]\n" if ($main::debug); + print "payload '$cmd' on fd=" . fileno($wfh) . "/" . + fileno($rfh) . "\n"; + + $self; +} + +sub restart { + my ($self) = @_; + + $self->initialise($self->{'args'}); +} + +# +# PAYLOAD SOCKET: handles a network socket as payload. +# +package PayloadSocket; +use base 'Payload'; + +sub initialise { + my ($self, $addr) = @_; + my ($payload); + + print "PayloadSocket::initialise [$self] addr<$addr>\n" + if ($main::debug); + + $self->SUPER::initialise(); + + $self->{'args'} = $addr; + + # Create a listening socket and add it to the multiplexor. + my $payload = new IO::Socket::INET(PeerAddr => $addr); + if (!$payload) { + $self->connected(0); + if ($self->can("restart")) { + my ($timeout) = $self->retry_timeout(); + + $self->cout->junctionInput($self, + "<<<PAYLOAD ERROR ($!) ... retrying in $timeout secs>>>\n"); + # Schedule a timeout to trigger a reconnect. + Callback->new($self->mux, $self, $timeout); + } else { + $self->cout->junctionEOF($self); + $self->cin->junctionRemove($self); + + Payload->lost($self->name, $self); + } + + } else { + $self->{'rfh'} = $payload; + $self->{'wfh'} = $payload; + + print "SHARE PAYLOAD: $self $payload (to $addr) [fd=" . + fileno($payload) . "]\n" if ($main::debug); + print "payload '$addr' on fd=" . fileno($payload) . "\n"; + $self->mux->add($payload); + + $self->mux->set_callback_object($self, $payload); + } + + print "SHARE PAYLOAD: $self $payload ... done\n" if ($main::debug); + + $self; +} + +sub restart { + my ($self) = @_; + + $self->initialise($self->{'args'}); +} + +# +# CLIENT: general client object, represents a remote client channel +# +package Client; + +sub new { + my ($class, $mux, $fh) = @_; + my $self = bless { 'mux' => $mux, + 'fh' => $fh }, $class; + + print "Client::new [$self] mux<$mux> fh<$fh>\n" + if ($main::debug); + + $self->initialise(); + + $self; +} + +sub clone { + my ($class, $from) = @_; + + my $self = bless { %{$from} }, $class; + + print "Client::clone [$self] from<$from>\n" if ($main::debug); + + $self->initialise(); + + $self; +} + +# Data accessors. +sub mux { + my $self = shift; + if (@_) { $self->{'mux'} = shift } + return $self->{'mux'}; +} +sub payload { + my $self = shift; + if (@_) { $self->{'payload'} = shift } + return $self->{'payload'}; +} +sub fh { + my $self = shift; + if (@_) { $self->{'fh'} = shift } + return $self->{'fh'}; +} +sub id { + my $self = shift; + if (@_) { $self->{'id'} = shift } + return $self->{'id'}; +} +sub announce { + my $self = shift; + if (@_) { $self->{'announce'} = shift } + return $self->{'announce'}; +} +sub cout { + my $self = shift; + if (@_) { $self->{'cout'} = shift } + return $self->{'cout'}; +} +sub cin { + my $self = shift; + if (@_) { + $self->{'cin'}->junctionRemove($self) if ($self->{'cin'}); + $self->{'cin'} = shift; + $self->{'cin'}->junctionAdd($self) if ($self->{'cin'} != undef); + } + return $self->{'cin'}; +} + +sub initialise { + my ($self) = @_; + + print "Client::initialise [$self]\n" if ($main::debug); + + $self->mux->set_callback_object($self, $self->fh); +} + +sub junctionInput { + my ($self, $from, $data) = @_; + + print "Client::junctionInput [$self] data<$data>\n" if ($main::debug); + + $self->mux->write($self->fh, $data); +} +sub junctionEOF { + my ($self, $from, $data) = @_; + + print "Client::junctionEOF [$self] data<$data>\n" if ($main::debug); + + $self->shutdown(); +} + +sub mux_eof { + my ($self, $mux, $fh, $input) = @_; + + print "Client::mux_eof [$self] mux<$mux> fh<$fh> input<$input>\n" + if ($main::debug); + + # Handle any pending input, then remove myself from the clients list. + $self->mux_input($mux, $fh, $input); + $self->cin(undef); + $self->cout(undef); + + # Tell the multiplexor we no longer are using this channel. + $mux->shutdown($fh, 1); +} +sub mux_close { + my ($self, $mux, $fn) = @_; + + print "Client::close [$self]\n" if ($main::debug); + + if ($self->announce) { + $self->announce->junctionInput($self, "(" . $self->id . + ") disconnected\n"); + } + print "$self->{'id'} disconnected\n"; +} + +sub shutdown { + my ($self) = @_; + + print "Client::shutdown [$self]\n" if ($main::debug); + + # Close myself down and tell the payload. + $self->mux->shutdown($self->fh, 2); +} +sub DESTROY { + my ($self) = @_; + + print "Client::DESTROY [$self]\n" if ($main::debug); +} + +# +# CLIENT CMD: represents a client whilst in command mode, when we have commited +# to connecting this will pass the client connection off to a ClientData +# object. +# +package ClientCmd; +use base 'Client'; + +sub mux_input { + my ($self, $mux, $fh, $input) = @_; + + print "Client::shutdown [$self] mux<$mux> fh<$fh> input<$$input>\n" + if ($main::debug); + + while ($$input =~ s/^(.*?)\n//) { + my ($cmd, $args) = split(' ', $1, 2); + my (%args) = Conmux::decodeArgs($args); + + my $reply = { + 'status' => 'ENOSYS unknown command', + }; + + # XXX: check authentication if required and reject the + # command out of hand - leak _nothing_. + if (!defined $args{'id'}) { + $reply->{'status'} = 'EACCES identifier required'; + goto reply; + } + # They are who they say they are, record who that is. + $self->{'id'} = $args{'id'}; + + if ($cmd eq "CONNECT") { + # Switch over to data mode, hand this connection off + # to a data client instance, I am done. + my ($data, $to, $in, $out); + $data = ClientData->clone($self); + + $to = $args{'to'}; + if (!$to) { + $reply->{'status'} = "EINVAL CONNECT " . + " requires 'to' specifier"; + goto reply; + } + my $payload = Payload->lookup($to); + if (!defined $payload) { + $reply->{'status'} = "EINVAL '$to' not a " . + " valid destination specifier"; + goto reply; + } + + $reply->{'status'} = 'OK'; + + # Get the payload title and pass that back. + $reply->{'title'} = $payload->title . ' [channel ' . + $payload->state() . ']'; + $reply->{'state'} = $payload->state(); + + $data->payload($payload); + $args{'type'} = 'client' if (!$args{'type'}); + if ($args{'type'} eq 'status') { + $data->cout($payload->cout); + } elsif ($args{'type'} eq 'client') { + if (!$args{'hide'}) { + $data->announce($payload->cout); + $payload->cout->junctionInput( + $self, "(" . $self->id . + ") connected\n"); + } + $data->cin($payload->cout); + $data->cout($payload->cin); + } else { + $reply->{'status'} = "EINVAL '$args{'type'}' " . + "not a valid destination type"; + goto reply; + } + + print "$self->{'id'} connected to $to/$args{'type'}\n"; + + $self->junctionInput($self, + Conmux::encodeArgs($reply) . "\n"); + + # Don't handle any more input - its not going to be + # for us. + last; + } + + reply: + # We're done, send back our response to this. + $self->junctionInput($self, Conmux::encodeArgs($reply) . "\n"); + } +} + +# +# CLIENT DATA: handles a client connection when in data mode, attaches +# the client connection to the relevant junction. +# +package ClientData; +use base 'Client'; + +my @help = ( + [ 'msg', 'send a message to all connected clients' ], + [ 'quit', 'disconnect from the console' ], +); +sub mux_input { + my ($self, $mux, $fh, $input) = @_; + + print "ClientData::mux_input [$self] mux<$mux> fh<$fh> input<$$input>\n" + if ($main::debug); + + while ($$input ne "") { + if ($self->{'cmd'} eq '') { + # Check for an incomplete escape ... wait for more. + if ($$input =~ /^~$/s) { + return; + } + if ($$input =~ s/^~\$//s) { + $self->{'cmd'} = '>'; + $self->junctionInput($self, "\r\nCommand> "); + next; + } + # Its not an escape ... pass it on. + # Ship anything before that cannot be the escape. + if ($$input =~ s/^(.[^~]*)(~|$)/\2/s) { + # Data coming in from the client, send it to + # the payload. + $self->cout->junctionInput($self, $1); + } + } else { + # Consume characters upto a newline, echo them back + # to the client as we go. + while ($$input =~ s/^([^\r\n])//) { + my $c = $1; + if ($c eq "\b" || $c eq "\x7f") { + if (length($self->{'cmd'}) > 1) { + $c = "\b \b"; + substr($self->{'cmd'}, + -1, 1, ''); + } else { + $c = ''; + } + } else { + $self->{'cmd'} .= $c; + } + $self->junctionInput($self, $c); + } + # If we arn't at a newline, then wait for more input. + if ($$input !~ s/^[\r\n]+//) { + return; + } + + $self->junctionInput($self, "\n"); + + my ($cmd, $a) = split(' ', substr($self->{'cmd'}, + 1), 2); + $self->{'cmd'} = ''; + + if ($cmd eq '') { + + } elsif ($cmd eq 'help') { + my @cmds = $self->payload->commandHelp(); + + my $ent; + my $help = "Conmux commands:\n"; + for $ent (@cmds, @help) { + $help .= sprintf(" %-20s %s\n", + $ent->[0], $ent->[1]); + } + $self->junctionInput($self, $help); + + } elsif ($cmd eq 'quit') { + $self->shutdown(); + + } elsif ($cmd eq 'msg') { + $self->cin->junctionInput($self, + "($self->{'id'}) $a\n"); + + # Not a client command ... pass it to the payload. + } else { + $self->payload->commandExec($self, $cmd, $a); + } + } + } +} + +# +# LIBRARY: split a string honouring quoting. +# +package main; +sub parse($) { + my ($str) = @_; + + my ($pos, @args, $argc, $quote, $real, $c, $inc); + + $inc = 0; + @args = (); + $argc = 0; + $quote = 0; + $real = 0; + + + $pos = 0; + while (substr($str, $pos, 1) eq " ") { + $pos++; + } + for (; $pos < length($str); $pos++) { + $c = substr($str, $pos, 1); + if ($quote != 2 && $c eq '\\') { + $real = 1; + $pos++; + $c = substr($str, $pos, 1); + } else { + $real = 0; + } + + if ($quote != 2 && $c eq '"' && !$real) { + $quote ^= 1; + } elsif ($quote != 1 && $c eq "'" && !$real) { + $quote ^= 2; + } elsif ($c eq " " && $quote == 0 && !$real) { + while (substr($str, $pos, 1) eq " ") { + $pos++; + } + $pos--; + $argc++; + } else { + if ($inc) { + $inc = 0; + $argc++; + } + $args[$argc] .= $c; + } + } + + @args; +} + +# +# MAIN: makes the IO multiplexor, junction, listener and payload and stitches +# them all together. +# +package main; + +# Usage checks. +if ($#ARGV != 0 && $#ARGV != 3) { + print STDERR "Usage: $P <config file>\n"; + print STDERR " $P <local port> <title> socket <host>:<port>\n"; + print STDERR " $P <local port> <title> cmd <cmd>\n"; + exit 1 +} +my @conf; +if ($#ARGV == 3) { + my ($lport, $title, $what, $arg) = @ARGV; + @conf = ( + "listener '$lport'", + "'$what' console '$title' '$arg'" + ); +} else { + my ($cf) = @ARGV; + open(CONF, '<', $cf) || die "$P: $cf: open failed - $!\n"; + @conf = <CONF>; + close(CONF); +} + +# Make a new multiplexer. +my $mux = new IO::Multiplex; + +my ($line, $seg, $listener, $payload); +$line = ''; +for $seg (@conf) { + # Handle comments, blank lines and line continuation. + chomp($seg); $seg =~ s/^\s+//; + next if ($seg =~ /^#/); + $line .= $seg; + if ($line =~ m/\\$/) { + chop($line); + next; + } + next if (/^\s+$/); + + my ($cmd, @a) = parse($line); + $line = ''; + + if ($cmd eq "listener") { + if ($#a != 0) { + warn "$P: Usage: listener <port>\n" . + "$P: $line\n"; + next; + } + + my ($lport) = @a; + my ($rhost, $rname); + + # port + if ($lport =~ m@^\d+$@) { + # Already in the right format. + + # registry/service + } elsif ($lport =~ m@(.*)/(.*)@) { + ($rhost, $rname, $lport) = ($1, $2, 0); + + # service + } else { + ($rhost, $rname, $lport) = ('-', $lport, 0); + } + + # Create the client listener socket. + $listener = ListenerSocket->new($mux, $lport); + + # Register us with the registry. + if ($rhost) { + Conmux::Registry::add($rhost, $rname, $listener->address); + } + + } elsif ($cmd eq 'socket') { + if ($#a != 2) { + warn "$P: Usage: socket <name> <title> <host:port>\n" . + "$P: $line\n"; + next; + } + my ($name, $title, $sock) = @a; + + # Create the payload. + $payload = PayloadSocket->new($name, $title, $mux, $sock); + + } elsif ($cmd eq 'application') { + if ($#a != 2) { + warn "$P: Usage: application <name> <title> <host:port>\n" . + "$P: $line\n"; + next; + } + my ($name, $title, $app) = @a; + + $payload = PayloadApplication->new($name, $title, $mux, $app); + + } elsif ($cmd eq 'command') { + if ($#a != 2) { + warn "$P: Usage: command <name> <msg> <cmd>\n" . + "$P: $line\n"; + next; + } + my ($name, $msg, $cmd) = @a; + + $payload->commandAdd($name, $msg, $cmd); + + } elsif ($cmd eq 'help') { + if ($#a != 1) { + warn "$P: Usage: $cmd <name> <msg>\n" . + "$P: $line\n"; + next; + } + my ($name, $msg) = @a; + + $payload->helpAdd($name, $msg); + } else { + warn "$P: $cmd: unknown configuration command\n"; + } +} + +# Hand over to the multiplexor. +do { + eval { $mux->loop; }; + warn "$@"; +} while ($@ =~ /^Use of freed value in iteration/); +die "ERROR: $@\n"; diff --git a/conmux/conmux-attach b/conmux/conmux-attach new file mode 100755 index 00000000..38ead0e8 --- /dev/null +++ b/conmux/conmux-attach @@ -0,0 +1,88 @@ +#!/usr/bin/perl +# +# conmux-attach -- attach commands to the console +# +# Attach stand alone commands to a console stream. You can specify +# which of the commands file descriptors are connected to which of the +# 'steams' in the multiplexor; the console input/output and the user +# output. +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +use IO::Socket; +use Getopt::Long qw(:config no_auto_abbrev); + +# Find our internal libraries. +###my $lib = $0; $lib =~ s@/[^/]+$@@; +###push(@INC, $lib); +###require Conmux; +##use lib "."; +##use Conmux; +use lib "/usr/local/conmux/lib"; +use Conmux; + +my $P = 'conmux-attach'; +my ($in, $out, $err); + +# Usage. +GetOptions( + 'i|stdout=s' => \$in, + 'o|stdin=s' => \$out, + 'e|stderr=s' => \$err, + 'b|bot=s' => \$bot, +) or exit; + +if ($in eq '' && $out eq '' && $err eq '') { + $in = 'client'; + $out = 'client'; +} + +if ($#ARGV < 1) { + print STDERR "$P: <host:port> <program> ..."; + exit 1; +} +my ($mux, $app) = @ARGV; +shift; +$app =~ s@.*/@@; $app =~ s@\s.*$@@; +$app = $bot if ($bot); + +# +# Connect to the client channel. +# +sub conmux_connect { + my ($mux, $type) = @_; + + if (!$cache{$mux, $type}) { + my $con = Conmux::connect($mux); + my %r = Conmux::sendCmd($con, 'CONNECT', { + 'id' => 'bot:' . $app, + 'to' => 'console', + 'type' => $type } ); + die "$P: $mux: connect failed - $r{'status'}\n" + if ($r{'status'} ne "OK"); + + $cache{$mux, $type} = $con; + } + + $cache{$mux, $type}; +} + +if ($in) { + my $to = conmux_connect($mux, $in); + open(STDIN, "<&", $to) || + die "$P: unable to hand off socket to stdin - $!\n"; +} +if ($out) { + my $to = conmux_connect($mux, $out); + open(STDOUT, ">&", $to) || + die "$P: unable to hand off socket to stdout - $!\n"; +} +if ($err) { + my $to = conmux_connect($mux, $err); + open(STDERR, ">&", $to) || + die "$P: unable to hand off socket to stderr - $!\n"; +} +exec @ARGV; diff --git a/conmux/conmux-registry b/conmux/conmux-registry new file mode 100755 index 00000000..7a4e0bfc --- /dev/null +++ b/conmux/conmux-registry @@ -0,0 +1,282 @@ +#!/usr/bin/perl +# +# conmux-registry -- console name registry server +# +# Main registry server. This server holds host/port assignments for +# conmux daemons registering with it. This allows users to specify +# human names for their consoles and find the relevant conmux daemon. +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +use strict; + +use Symbol qw(gensym); + +use IO::Socket; +use IO::Multiplex; +use URI::Escape; + +# Find our internal libraries. +###my $lib = $0; $lib =~ s@/[^/]+$@@; +###push(@INC, $lib); +###require Conmux; +##use lib "."; +##use Conmux; +use lib "/usr/local/conmux/lib"; +use Conmux; + +our $P = 'conmux-registry'; +our $debug = 0; + +# +# LISTENER SOCKET: creates an intenet listener for new clients and +# connects them to the junction provided. +# +package ListenerSocket; + +sub new { + my ($class, $mux, $port, $registry) = @_; + my $self = bless { 'mux' => $mux, 'registry' => $registry }, $class; + + print "ListenerSocket::new [$self] mux<$mux> port<$port> " . + "registry<$registry>\n" if ($main::debug); + + $self->initialise($mux, $port, $registry); + + $self; +} + +sub initialise { + my ($self, $mux, $port, $registry) = @_; + my ($sock); + + print "ListenerSocket::initialise [$self] mux<$mux> port<$port> " . + "registry<$registry>\n" if ($main::debug); + + # Create a listening socket and add it to the multiplexor. + my $sock = new IO::Socket::INET(Proto => 'tcp', + LocalPort => $port, + Listen => 4, + ReuseAddr => 1) + or die "socket: $@"; + + print " adding $self $sock\n" if ($main::debug); + $mux->listen($sock); + $mux->set_callback_object($self, $sock); + $self->{'listener'} = $sock; +} + +# Handle new connections by instantiating a new client class. +sub mux_connection { + my ($self, $mux, $fh) = @_; + my ($client); + + print "ListenerSocket::mux_connection [$self] mux<$mux> fh<$fh>\n" + if ($main::debug); + + # Make a new client connection. + $client = Client->new($mux, $fh, $self->{'registry'}); + print " new connection $self $client\n" if ($main::debug); +} + +sub DESTROY { + my ($self) = @_; + + print "ListenerSocket::DESTROY [$self]\n" if ($main::debug); + + close($self->{'listener'}); +} + +# +# CLIENT: general client object, represents a remote client channel +# +package Client; + +sub new { + my ($class, $mux, $fh, $registry) = @_; + my $self = bless { 'mux' => $mux, + 'fh' => $fh, + 'registry' => $registry }, $class; + + print "Client::new [$self] mux<$mux> fh<$fh> registry<$registry>\n" + if ($main::debug); + + $self->initialise($mux, $fh, $registry); + + $self; +} + +sub initialise { + my ($self, $mux, $fh, $registry) = @_; + + print "Client::initialise [$self] mux<$mux> fh<$fh> " . + "registry<$registry>\n" if ($main::debug); + + $mux->set_callback_object($self, $fh); +} + +sub mux_input { + my ($self, $mux, $fh, $input) = @_; + + print "Client::mux_input [$self] mux<$mux> fh<$fh> input<$$input>\n" + if ($main::debug); + + while ($$input =~ s/^(.*?)\n//) { + my ($cmd, $args) = split(' ', $1, 2); + my (%args) = Conmux::decodeArgs($args); + + my $reply = { + 'status' => 'ENOSYS', + }; + + # Fill in the common results. + $reply->{'title'} = 'registry'; + + # Handle this command. + if ($cmd eq "LOOKUP") { + my $r = $self->{'registry'}->lookup($args{'service'}); + + if (defined $r) { + $reply->{'result'} = $r; + $reply->{'status'} = 'OK'; + + } else { + $reply->{'status'} = 'ENOENT entry not found'; + } + + } elsif ($cmd eq "ADD") { + $self->{'registry'}->add($args{'service'}, + $args{'location'}); + $reply->{'status'} = 'OK'; + + } elsif ($cmd eq "LIST") { + $reply->{'result'} = $self->{'registry'}->list(); + $reply->{'status'} = 'OK'; + } + + $fh->write(Conmux::encodeArgs($reply) . "\n"); + } +} +sub mux_eof { + my ($self, $mux, $fh, $input) = @_; + + print "Client::mux_eof [$self] mux<$mux> fh<$fh> input<$input>\n" + if ($main::debug); + + # Handle any pending input, then remove myself. + $self->mux_input($mux, $fh, $input); + + # Tell the multiplexor we no longer are using this channel. + $mux->shutdown($fh, 1); +} +sub mux_close { + my ($self, $mux, $fn) = @_; + + print "Client::close [$self]\n" if ($main::debug); +} + +sub DESTROY { + my ($self) = @_; + + print "Client::DESTROY [$self]\n" if ($main::debug); +} + +# +# REGISTRY: registry elements. +# +package Registry; + +sub new { + my ($class, $store) = @_; + my $self = bless { 'store' => $store }, $class; + + my ($key, $val); + + print "Registry::new [$self] store<$store>\n" if ($main::debug); + + # Open the store and populate the keys. + open(S, '<', $store) || die "Registry::new: $store: open failed - $!\n"; + while (<S>) { + chomp; + + ($key, $val) = split(' ', $_); + + $self->{'key'}->{$key} = $val; + } + close(S); + + $self; +} + +sub add { + my ($self, $what, $where) = @_; + + my ($key); + + print "Registry::add [$self] what<$what> where<$where>\n" + if ($main::debug); + + $self->{'key'}->{$what} = $where; + + print "$what at $where\n"; + + if (open(S, '>', $self->{'store'} . '.new')) { + foreach $key (sort keys %{$self->{'key'}}) { + print S "$key $self->{'key'}->{$key}\n"; + } + close(S); + rename $self->{'store'} . '.new', $self->{'store'}; + + } else { + warn "$P: $self->{'store'}.new: open failed - $!"; + } +} + +sub lookup { + my ($self, $what) = @_; + + print "Registry::lookup [$self] what<$what>\n" if ($main::debug); + + $self->{'key'}->{$what}; +} + +sub list { + my ($self) = @_; + my ($r, $key); + + print "Registry::list [$self]\n" if ($main::debug); + + foreach $key (sort keys %{$self->{'key'}}) { + $r .= "$key $self->{'key'}->{$key}\n"; + } + + $r; +} + +# +# MAIN: makes the IO multiplexor, listener and registry and stitches +# them all together. +# +package main; + +# Usage checks. +if ($#ARGV != 1) { + print STDERR "Usage: $P <local port> <store>\n"; + exit 1 +} +my ($lport, $store) = @ARGV; + +# Make a new multiplexer. +my $mux = new IO::Multiplex; + +# Make the registry object. +my $registry = Registry->new($store); + +# Create the client listener socket. +my $listener = ListenerSocket->new($mux, $lport, $registry); + +# Hand over to the multiplexor. +$mux->loop; diff --git a/conmux/conmux.html b/conmux/conmux.html new file mode 100644 index 00000000..9f9cbeaf --- /dev/null +++ b/conmux/conmux.html @@ -0,0 +1,158 @@ +<html> +<!-- +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +--> +<head> +<title>conmux - the console multiplexor</title> +<style type="text/css"> +.example { white-space: pre; margin-left: 2em; margin-right: 2em; + font-family: monospace } +table.quote { margin-left: 2em; margin-right: 2em } +</style> +</head> + +<body> +<center> +<h1>conmux - the console multiplexor</h1> +</center> + +<p><em>conmux</em>, the console multiplexor is a system designed to abstract +the concept of a console. That is to provide a virtualised machine interface, +including access to the console and the 'switches' on the front panel; the +/dev/console stream and the reset button. It creates the concept of a virtual +console server for multiple consoles and provides access to and sharing of +consoles connected to it. + +<p>There are two main motivations for wanting to do this. Firstly, we have +many different machine types with vastly differing access methodologies for +their consoles and for control functions (VCS, HMC, Annex) and we neither want +to know what they are nor how they function. Secondly, most console sources +are single access only and we would like to be able to share the console data +between many consumers including users. + +<h2>Basic Usage</h2> + +<p>The main interface to the consoles is via the <code>console</code> +program. This connects us to the console server for the machine and allows us +to interact with it, including issuing <i>out-of-band</i> commands to control +the machine. + +<div class="example">$ console <host>/<console> +</div> + +<p>In the example below we indicate that the console we require is located on +the virtual console server <code>consoles.here.com</code> and the +specific console is <code>elm3b70</code>. + +<div class="example">$ console consoles.here.com/elm3b70 +Connected to elm3b70 console (~$quit to exit) + +Debian GNU/Linux 3.1 elm3b70 ttyS0 + +elm3b70 login: +</div> + +<p>Once connected we can interact normally with the console stream. To +perform front pannel operation such as peforming an <em>hard reset</em> we +switch to command mode. This is achieved using the escape sequence +<code>~$</code>. Nore the prompt <code>Command></code> + +<div class="example">elm3b70 login: <em>~$</em> +Command> quit +Connection closed +$ +</div> + +<h2>Command Summary</h2> + +<p>The following commands are generally available: + +<p><table class="quote" rules="all" frame="box"> + +<tr><th>Command<th>Description</tr> + +<tr><td>quit<td>quit this console session, note that this disconnects us from +the session it does not affect the integity of the session itself.</tr> + +<tr><td>hardreset<td>force a hard reset on the machine, this may be a simple +reset or a power off/on sequence whatever is required by this system.</tr> + +</table> + +<h2>Architecture</h2> + +<p>The conmux provides a virtual console multiplexor system reminicent of +an Annex terminal server. You refer to the conmux <em>server</em> and +<em>lines</em>, unlike an Annex lines are referred to by mnemonic names. +Above we referred to the console for <code>elm3b70</code> 'connected to' the +server <code>consoles.here.com</cond>. A virtual console server consists +of a number of server processes. One <code>conmux-registry</code> server, +several <code>conmux</code> servers and optionally several <em>helper</em> +processes. + +<p><code>conmux-registry</code>: a server is defined by the server registry. +This maintains the mnemonic name to current server location relation. When +a client wishes to attach to a console on a server, the registry is first +queried to locate the server currently handling that console. + +<p><code>conmux</code>: for each connected console there is a corresponding +console multiplexor. This process is responsible for maintaining the +connection to the console and for redistributing the output to the various +connected clients. It is also responsible for handling "panel" commands +from the client channels. + +<p><code>autoboot-helper</code>: an example helper which aids systems which +are not capable of an automatic reboot. It connects to a console and watches +for tell-tale reboot activity, preforming a "panel" <em>hardreset</em> when +required. This provides the impression of seamless reboot for systems +which this does not work. + +<h2>Configuration</h2> + +<h3>conmux-registry</h3> + +<p>Configuration of this service is very simple. Supplying the default +registry port (normally 63000) and the location for the persistant registry +database. + +<h3>conmux</h3> + +<p>Configuration of each conmux is complex. Each has a listener, payload +and optionally one or more panel commands. Configuration is provided via +a per console configuration file. This file consists of lines defining each +element: + +<p><code>listener <server>/<name></code>: +defines the name of this console port as it appears in the registry. + +<p><code>socket <name> <title> <host>:<port></code>: +defines a console payload connected to a tcp socket on the network. +<code>name</code> defines this payload within the multiplexor, +<code>title</code> is announced to the connecting clients. + +<p><code>application <name> <title> <cmd></code>: +defines a console payload which is accessed by running a specific command. +<code>name</code> defines this payload within the multiplexor, +<code>title</code> is announced to the connecting clients. + +<p><code>command <panel> <message> <cmd></code>: +defines a panel command for the preceeding payload, triggerd when +<code>panel</code> is typed at the command prompt. <code>message</code> +is announced to the user community. <code>cmd</cmd> will be actually +executed. + +<p>For example here is the configuration for a NUMA-Q system which is rebooted +using a remote VCS console and for which the real console channel is +on an Annex terminal server: + +<div class="example">listener localhost/elm3b130 +socket console 'elm3b130 console' console.server.here.com:2040 + command 'hardreset' 'initated a hard reset' \ + './reboot-numaq vcs 1.2.3.4 elm3b130 12346 Administrator password' +</div> + +</body> +</html> diff --git a/conmux/console b/conmux/console new file mode 100755 index 00000000..a7ad2ab6 --- /dev/null +++ b/conmux/console @@ -0,0 +1,192 @@ +#!/usr/bin/perl +# +# console <host>/<machine> -- interactive client interface +# +# The main interactive client interace to conmux. Allows direct +# interaction with the payload, as well as allowing break out +# to the conmux menu to envoke defined commands; for example +# hardreset. +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +$| = 1; + +our $P = 'console'; + +use IO::Socket; +use IO::Multiplex; +use Getopt::Long qw(:config no_auto_abbrev); + +my $CONMUX = '/usr/local/conmux'; +my $CONMUX = $ENV{'CONMUX_ROOT'} if ($ENV{'CONMUX_ROOT'}); + +# Find our internal libraries. +###my $lib = $0; $lib =~ s@/[^/]+$@@; +###push(@INC, $lib); +###require Conmux; +##use lib "."; +##use Conmux; +use lib "/usr/local/conmux/lib"; +use Conmux; + +# Basic terminal handling. +sub termRaw { + $termSettings = `stty -g`; + system "stty raw -echo opost onlret"; +} +sub termRestore { + system "stty $termSettings"; +} + +my $bot; +my $list; +my $status; +GetOptions( + 'b|bot=s' => \$bot, + 'l|list' => \$list, + 's|status' => \$status, +); +sub usage { + warn "Usage: $P <service>\n"; + warn " $P <registry>/<service>\n"; + warn " $P <host>:<port>\n"; + warn " $P --status <service>\n"; + die " $P --list [<registry>]\n"; +} + +my $id; +if ($bot) { + $id = 'bot:' . $bot; +} else { + $id = 'user:' . $ENV{'LOGNAME'}; +} + +# +# MODE: registry list. +# +if ($list) { + if ($#ARGV == -1) { + print Conmux::Registry::list('-'); + + } elsif ($#ARGV == 0) { + print Conmux::Registry::list($ARGV[0]); + + } else { + usage(); + } + exit 0 +} + +# +# COMMAND: payload status command +# +if ($status) { + usage() if ($#ARGV != 0); + + my $sock; + eval { + $sock = Conmux::connect($ARGV[0]); + }; + if ($@) { + print "unavailable\n"; + exit 0 + } + my %r = Conmux::sendCmd($sock, 'CONNECT', { 'id' => $id, + 'to' => 'console', 'hide' => 1 }); + if ($r{'status'} ne 'OK') { + print "unavailable\n"; + + } elsif ($r{'state'}) { + print "$r{'state'}\n"; + + } else { + print "unknown\n"; + } + exit 0; +} + + +# +# COMMAND: general payload connect. +# +if ($#ARGV != 0) { + usage(); +} + +# Create a multiplex object +my $mux = new IO::Multiplex; + +# Connect to the host/port specified on the command line, +# or localhost:23 +my $sock = Conmux::connect($ARGV[0]); + +my %r = Conmux::sendCmd($sock, 'CONNECT', { 'id' => $id, 'to' => 'console' }); +die "$P: $ARGV[0]: connect failed - $r{'status'}\n" if ($r{'status'} ne 'OK'); + +print "Connected to $r{'title'} (~\$quit to exit)\n"; + +# OK, we are now connected ... add the relevant file handles to the mux +$mux->add($sock); +$mux->add(\*STDIN); +# We want to buffer output to the terminal. This prevents the program +# from blocking if the user hits CTRL-S for example. +$mux->add(\*STDOUT); + +# We're not object oriented, so just request callbacks to the +# current package +$mux->set_callback_object(__PACKAGE__); + +termRaw(); + +# Enter the main mux loop. +$mux->loop; + +# mux_input is called when input is available on one of +# the descriptors. +sub mux_input { + my $package = shift; + my $mux = shift; + my $fh = shift; + my $input = shift; + + # Figure out whence the input came, and send it on to the + # other place. + if ($fh == $sock) { + print STDOUT $$input; + + # Remove the input from the input buffer. + $$input = ''; + } else { + print $sock $$input; + + # Remove the input from the input buffer. + $$input = ''; +## while ($$input ne "") { +## # Check for an incomplete escape ... wait for more. +## if ($$input =~ /^~$/s) { +## return; +## } +## if ($$input =~ s/^~\$//s) { +## $mux->shutdown($sock, 1); +## } +## # Its not an escape ... pass it on. +## # Ship anything before that cannot be the escape. +## if ($$input =~ s/^(.[^~]*)(~|$)/\2/s) { +## print $sock $1; +## } +## } + } +} + +# This gets called if the other end closes the connection. +sub mux_eof { + print STDERR "Connection Closed\n"; + termRestore(); + exit; +} +sub mux_close { + print "CLOSE\n"; +} diff --git a/conmux/drivers/blade b/conmux/drivers/blade new file mode 100755 index 00000000..76a839af --- /dev/null +++ b/conmux/drivers/blade @@ -0,0 +1,234 @@ +#!/usr/bin/expect -- +# +# blade -- console and management control for IBM blade servers. +# +# Obtain consoles for and hard reset support for blades in +# IBM blade centres. Blades are identified by their blade centre ids. +# +# usage: +# blade reboot|console <blade id> <user> <password> <connect command> ... +# +# example: +# blade reboot blade[4] FOO BAR telnet 1.2.3.4 +# blade console blade[4] FOO BAR telnet 1.2.3.4 +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +set P "blade" + +if { [llength $argv] < 5 } { + puts stderr "Usage: $P <command> <blade> <login> <password> <connect command ...>" + exit 1 +} + +log_user 0 + +set cmd [lindex $argv 0] +set system [lindex $argv 1] +set username [lindex $argv 2] +set password [lindex $argv 3] +set command [lrange $argv 4 end] + +# Validate the command. +switch -- $cmd { + console {} + reboot {} + default { + puts stderr "$P: $cmd: unrecognised command"; + exit 1 + } +} +# Ensure the blade name is fully qualified. This ensures that +# the console prompt detection as reliable as possible. Add the system +# prefix if it was not specified. +switch -re -- "$system" { + system:.* { } + .* { set system "system:$system" } +} + +#log_file -a "$logfile" + +set elapsed_time 0 +set timeout 30 + +proc note {m} { + global P + puts "$P: $m" +} +proc warn {m} { + global P + puts "$P: WARNING: $m" +} +proc winge {m} { + global P + puts "$P: ERROR: $m" +} + +note "Logging into blade centre with command \"$command\" to restart it"; + +# CONNECT: connect to the remote console. +eval spawn $command +expect { + default { + winge "login prompt not issued" + exit 2 + } + "Connection closed by foreign host." { + winge "Telnet connection closed." + exit 1 + } + "Unable to connect to remote host:" { + winge "Connection to remote console failed" + exit 2 + } + "username:" { + #note "saw login prompt" + } +} + +# AUTHENTICATE: send username and password at the relevant prompts +note "sending login ..." +send -- "$username\r" +expect { + default { + winge "password prompt not issued" + exit 2 + } + "password:" { + #note "password prompt found" + } +} + +note "sending password ..." +send -- "$password\r" +expect { + default { + winge "command prompt not issued" + exit 2 + } + "Invalid login!" { + winge "login/password incorrect ... aborting" + exit 1 + } + -- "system>" { + #note "command prompt found" + } +} + +# SYSTEM: first validate the system specifier, if it does not +# exist get a listing and print that out whilst we are connected. +note "selecting system '$system' ..." +send "env -T $system\r" +set found 0 +set prompt "$system>" +expect { + default { + winge "command prompt not issued" + exit 2 + } + OK { + set found 1 + exp_continue + } + -- "system>" { + #note "command prompt found" + } + -ex $prompt { + #note "command prompt found" + } +} + +# The system the user specified was not found, give them a hand by +# getting a list of systems. +if {$found == 0} { + note "Defined systems:" + send "list -l 2\r" + expect { + default { + winge "command prompt not issued" + exit 2 + } + -- "system>" { + #note "command prompt found" + } + "\n$" { + puts -nonewline "$expect_out(buffer)" + exp_continue + } + } + note "complete ... exiting" + send "exit\r" + exit 1 +} + +# CONSOLE: open a console channel +if {[string compare $cmd "console"] == 0} { + note "connecting to console ..." + send "console -o\r" + interact { + -o -ex $prompt { + return + } + } + winge "console lost" + exit 0 +} + +# Function to wait until the issued power command has taken effect. +proc powerWait {s} { + global prompt + set ok 0 + for {set retry 1} {$ok == 0 && $retry < 30} {incr retry} { + note "checking for power state $s ..." + after 2000 + send "power -state\r" + expect { + default { + winge "command prompt not issued" + exit 2 + } + -ex $s { + set ok 1 + exp_continue + } + -ex "$prompt" { + #note "command prompt found" + } + } + } + if {$ok == 0} { + winge "system did not enter power $s state ... aborting" + exit 2 + } +} + +# DOWN: shut the system down ... hard. Expect to OK before the +# prompt in the case of success. +note "powering cycling the system ..." +send "power -cycle\r" +set fail 1 +expect { + default { + winge "command prompt not issued" + exit 2 + } + "OK" { + set fail 0 + exp_continue + } + -ex "$prompt" { + #note "command prompt found" + } +} +if {$fail == 1} { + winge "power cycle failed ... system not rebooted" + exit 2 +} +powerWait On + +note "complete ... exiting" +send "exit" +exit 0 diff --git a/conmux/drivers/hmc b/conmux/drivers/hmc new file mode 100755 index 00000000..13894f43 --- /dev/null +++ b/conmux/drivers/hmc @@ -0,0 +1,597 @@ +#!/usr/bin/expect -- +# +# hmc -- handle consoles and rebooting of HMC based systems +# +# Allow connecting to the console of and rebooting of HMC connected +# machines. Machines are identified by the HMC IP address, connected +# system name, and partition name. +# +# usage: +# hmc open-term|reboot -h <hmc IP> -m <system> -p <partition> \ +# -U <user> -P <password> -V <hmc version> +# +# example: +# hmc open-term -m elm3b70 -p FullSystemPartition -U hscroot -P passwd \ +# -V 2.6 -h 1.2.3.4 +# hmc reboot -m elm3b70 -p FullSystemPartition -U hscroot -P passwd \ +# -V 2.6 -h 1.2.3.4 +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +set P "hmc" + +# See interactions. +log_user 0 +#exp_internal 1 + +proc note {m} { + global P + puts "$P: $m" +} +proc warn {m} { + global P + puts "$P: WARNING: $m" +} +proc winge {m} { + global P + puts "$P: MACHINE ERROR: $m" +} + +# +# OPTIONS: options parser. +# +proc shift {_list} { + upvar $_list list + + set res [lindex $list 0] + set list [lreplace $list 0 0] + + return $res +} +proc arg {_list arg} { + upvar $_list list + + if {[llength $list] < 1} { + winge "$arg: required argument missing" + exit 1 + } + + return [shift list] +} + +set user {hscroot} +set host {} +set system {} +set lpar {} +set profile {default} +set mode {norm} +set passwd {} +set version {} + +set cmd [lindex $argv 0] +shift argv +while {[llength $argv] > 0} { + switch -- [shift argv] { + -h { set host [arg argv h]} + hmc { set host [arg argv h]} + -m { set system [arg argv m]} + -l { set lpar [arg argv l]} + -p { set lpar [arg argv p]} + -f { set profile [arg argv f]} + -b { set mode [arg argv b]} + -U { set user [arg argv P]} + -P { set passwd [arg argv P]} + -V { set version [arg argv P]} + } +} + +if {[llength $argv] > 0} { + puts stderr "Usage: $P <cmd> -h <hmc> -m <system> -p <lpar> -f <profile> -b <mode>" + exit 1 +} +if {[string compare $host ""] == 0 || + [string compare $system ""] == 0 || + [string compare $lpar ""] == 0} \ +{ + winge "hmc (-h), machine (-m) and lpar (-p) required" + exit 1 +} + +set prompt {___EOF___HMC___EOF___} +set hscpath {/opt/hsc/bin} + +#log_file -a "$logfile" + +set elapsed_time 0 +set timeout 30 + +# Ensure we have a terminal so we don't get prompted for one. +if {![info exists env(TERM)]} { + set env(TERM) {vt100} +} + +# If we have a password don't use any ssh-keys we may have been offered. +if {[string compare $passwd ""] == 0} { + set command "ssh $env(SSH_OPTIONS) $user@$host" +} else { + set command "ssh $user@$host" +} + +# CONNECT: connect to the remote console. Set the prompt up so we +# know when a command we execute has completed. +note "Logging into HMC console with command \"$command\" ..." +eval spawn $command +send "PS1=$prompt\r" +expect { + "password:" { + if {[string compare $passwd ""] == 0} { + winge "$host: requesting password - not hacked" + exit 2 + } + send "$passwd\r" + after 500 + send "PS1=$prompt\r" + exp_continue + } + "Connection timed out" { + winge "$host: cannot connect to console" + exit 2 + } + "Name or service not known" { + winge "$host: HMC name invalid" + exit 1 + } + "Permission denied" { + winge "$host: authentication failure, permission denied" + exit 2 + } + timeout { + winge "$host: HMC did not prompt us, timed out" + exit 2 + } + eof { + winge "$host: HMC disconnected without prompting" + exit 2 + } + + "Terminal type?" { + send "vt100\r" + send "PS1=$prompt\r" + exp_continue + } + + "PS1=$prompt" { + # Ignore us typing the change to the prompt. + exp_continue + } + "$prompt" { + # Prompt see, we are in and working, drop out. + ## note "prompt seen" + } + + -re "^\[^\n]*\n" { + # We need to absorb any login information, motd etc. + ## puts "NOISE<$expect_out(buffer)>" + exp_continue + } +} + +proc runit {cmd _out} { + upvar $_out out + + global prompt + + send "$cmd" + + set text "" + set line 0 + + set timeout 120 + expect { + + -re "^\[^\n]*\n" { + ##note "LINE: $expect_out(buffer)" + if { $line > 0 } { + append text $expect_out(buffer) + } + incr line + exp_continue + } + + -re "$prompt" { + ##note "prompt seen" + } + + timeout { + set out "hmc: command timeout" + return 255 + } + } + set out [string trimright $text] + + send "echo \$?\r" + set text "" + set line 0 + set timeout 30 + expect { + -re "^\[^\n]*\n" { + ##note "LINE: $expect_out(buffer)" + if { $line > 0 } { + append text $expect_out(buffer) + } + incr line + exp_continue + } + + -re "$prompt" { + ##note "prompt seen" + } + + timeout { + set out "hmc: command status timeout" + return 255 + } + } + ##note "EXIT: $text" + + return [string trimright $text] +} + +proc extract_names {out} { + set res {} + foreach sys [split $out] { + if {[string compare $sys ""] != 0} { + lappend res $sys + } + } + + return $res +} + +# Find out the current version of the HMC software. +if {[string compare $version ""] == 0} { + runit "rpm -q IBMhsc.coreserver --qf '%{VERSION}'; echo ''\r" version +} +if {[string compare $version ""] == 0} { + winge "unable to obtain HMC code version" + exit 1 +} +note "HMC revision v$version" + +# Based on the version of the HMC software select payload. +if {$version < 4.0} { + # + # VERSION 2.6/3.0 specific support. + # + proc system_list {} { + global hscpath + + # Ask for a list of systems. + if {[runit "lssyscfg -r sys --all -F name\r" out] != 0} { + winge "unable to obtain list of systems" + exit 1 + } + + return [extract_names $out] + } + proc system_state {system} { + global hscpath + + # Ask for the system status. + if {[runit "lssyscfg -r sys -n $system -F state\r" out] != 0} { + winge "$system: failed to get status" + exit 1 + } + + return $out + } + proc lpar_list {system} { + global hscpath + + # Ask for a list of lpars for this system. + if {[runit "lssyscfg -r lpar -m $system --all -F name\r" out] != 0} { + winge "unable to obtain list of lpars" + exit 1 + } + + return [extract_names $out] + } + proc state {system lpar} { + global hscpath + + # Ask for the lpar status. + if {[runit "lssyscfg -r lpar -m $system -n $lpar -F state\r" out] != 0} { + winge "$system/$lpar: failed to get lpar status" + exit 1 + } + + return $out + } + proc reboot_metal {system lpar profile mode} { + global hscpath + + # If we have no connection wait for the machine to appear. + # XXX: timeout? + set was {} + set when [clock seconds] + if {[string compare [system_state $system] "Ready"] != 0} { + while {([clock seconds] - $when) < 60} { + sleep 15 + set state [system_state $system] + + note "waiting for stable connected state ($state)" + if {$state != $was || [string compare $state "No Connection"] == 0} { + set was $state + set when [clock seconds] + } + } + } + + # See if the system is is in Error, if so attempt to + # power it on. + if {[string compare [system_state $system] "Error"] == 0} { + note "starting full system (from Error)" + if {[runit "chsysstate -r sys -m $system -n $system -b $mode -o on -c full\r" out] == 0} { + note "started" + return + } + note "start failed - attempting shutdown" + } + + # See if the system is up, if so shut it down. + if {[string compare [system_state $system] "No Power"] != 0} { + note "shutting down full system" + if {[runit "chsysstate -r sys -m $system -n $system -o off -c full\r" out] != 0} { + winge "$system: power off failed\n$out" + exit 2 + } + } + + while {[string compare [system_state $system] "No Power"] != 0} { + note "waiting for shutdown" + sleep 15 + } + + note "starting full system" + if {[runit "chsysstate -r sys -m $system -n $system -b $mode -o on -c full\r" out] != 0} { + winge "$system: power on failed" + exit 2 + } + note "started" + } + proc reboot {system lpar profile mode} { + global hscpath + + # Handle the bare metal separatly. + if {[string compare $lpar "FullSystemPartition"] == 0} { + return [reboot_metal $system $lpar $profile $mode] + } + + # Partitions in Error state are tricky, you _may_ either + # have to shut them down and then start them, or you may + # just have to start them. So, if we are in Error start + # the partition, if it fails we can just drop through + # to the regular stop/start cycle. + if {[string compare [state $system $lpar] "Error"] == 0} { + note "starting lpar (from Error)" + if {[runit "chsysstate -r lpar -m $system -n $lpar -b $mode -o on\r" out] == 0} { + note "started" + return + } + note "start failed - attempting shutdown" + } + + # See if the lpar is up, if so shut it down. + if {[string compare [state $system $lpar] "Ready"] != 0} { + note "shutting down lpar" + if {[runit "chsysstate -r lpar -m $system -n $lpar -o off\r" out] != 0} { + winge "$system/$lpar: power off failed\n$out" + exit 2 + } + } + + while {[string compare [state $system $lpar] "Ready"] != 0} { + note "waiting for shutdown" + sleep 15 + } + + note "starting lpar" + if {[runit "chsysstate -r lpar -m $system -n $lpar -b $mode -o on\r" out] != 0} { + winge "$system/$lpar: power on failed\n$out" + exit 2 + } + note "started" + } + +} + +if {$version >= 4.0} { + # + # VERSION 4.x specific support. + # + proc system_list {} { + global hscpath + + # Ask for a list of systems. + if {[runit "lssyscfg -r sys -F name\r" out] != 0} { + winge "unable to obtain list of systems" + exit 1 + } + + return [extract_names $out] + } + proc system_state {system} { + global hscpath + + # Ask for the system status. + if {[runit "lssyscfg -r sys -m $system -F state\r" out] != 0} { + winge "$system: failed to get system status" + exit 1 + } + + return $out + } + proc lpar_list {system} { + global hscpath + + # Ask for a list of lpars for this system. + if {[runit "lssyscfg -r lpar -m $system -F name\r" out] != 0} { + winge "unable to obtain list of lpars" + exit 1 + } + + return [extract_names $out] + } + proc state {system lpar} { + global hscpath + + # Ask for the lpar status. + if {[runit "lssyscfg -r lpar -m $system --filter lpar_names=$lpar -F state\r" out] != 0} { + winge "$system/$lpar: failed to get lpar status" + exit 1 + } + + return $out + } + + proc reboot {system lpar profile mode} { + global hscpath + + # Partitions in Error state are tricky, you _may_ either + # have to shut them down and then start them, or you may + # just have to start them. So, if we are in Error start + # the partition, if it fails we can just drop through + # to the regular stop/start cycle. + if {[string compare [state $system $lpar] "Error"] == 0} { + note "starting lpar (from Error)" + if {[runit "chsysstate -r lpar -m $system -n $lpar -f $profile -b $mode -o on\r" out] == 0} { + note "started" + return + } + note "start failed - attempting shutdown" + } + + # See if the lpar is up, if so shut it down. + if {[string compare [state $system $lpar] "Not Activated"] != 0} { + note "shutting down lpar" + if {[runit "chsysstate -r lpar -m $system -n $lpar -o shutdown --immed\r" out] != 0} { + winge "$system/$lpar: power off failed\n$out" + exit 2 + } + } + + while {[string compare [state $system $lpar] "Not Activated"] != 0} { + note "waiting for shutdown" + sleep 15 + } + + note "starting lpar" + if {[runit "chsysstate -r lpar -m $system -n $lpar -f $profile -b $mode -o on\r" out] != 0} { + winge "$system/$lpar: power on failed\n$out" + exit 2 + } + note "started" + } +} + +# +# VERSION: common +# +proc open-term {system lpar} { + global prompt + global hscpath + global spawn_id + + note "opening console ..." + send "ulimit -t 3600; \[ -f cleandown ] && sudo /home/hscroot/cleandown $system $lpar; mkvterm -m $system -p $lpar\r" + expect { + "NVTS" { } + "Open in progress.." { } + "$prompt" { + winge "$system/$lpar: open console failed" + exit 2 + } + } + + note "console open" + interact { + -i $spawn_id + " Connection has closed" { + winge "$system/$lpar: console connection closed" + exit 2 + } + {Error in communication path to the partition} { + winge "$system/$lpar: lost contact with lpar" + exit 2 + } + "$prompt" { + winge "$system/$lpar: open console failed" + exit 2 + } + eof { + winge "$system/$lpar: payload lost" + exit 2 + } + \000 { + } + + } + note "console closed" +} + +proc close-term {system lpar} { + global hscpath + + note "closing console ..." + if {[runit "rmvterm -m $system -p $lpar\r" out] != 0} { + winge "$system/$lpar: close console failed" + exit 2 + } +} + +# Look and see if this system exists. +set systems [system_list] +if {[lsearch -exact $systems $system] < 0} { + winge "$system: system not known; console knows: $systems" + exit 1 +} +set lpars [lpar_list $system] +if {[lsearch -exact $lpars $lpar] < 0} { + winge "$system/$lpar: lpar not known; console knows: $lpars" + exit 1 +} + +# Check for system in Operating/Ready. +set state [system_state $system] +if {[string compare $lpar "FullSystemPartition"] != 0 && + [string compare $state "Operating"] != 0 && + [string compare $state "Ready"] != 0} \ +{ + winge "$system: unusable - $state" + exit 1 +} + +# Ask for the lpar status, to see if it exists. +set lstate [state $system $lpar] +note "$system/$lpar: found ($state/$lstate)" + +# +# COMMANDS: command. +# +switch -- $cmd { + {open-term} { + close-term $system $lpar + open-term $system $lpar + } + {close-term} { + close-term $system $lpar + } + {reboot} { + reboot $system $lpar $profile $mode + } +} + +exit 0 diff --git a/conmux/drivers/module.mk b/conmux/drivers/module.mk new file mode 100755 index 00000000..1c28816d --- /dev/null +++ b/conmux/drivers/module.mk @@ -0,0 +1,15 @@ +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 + +DRIVERS:=blade hmc reboot-netfinity reboot-newisys reboot-numaq \ + reboot-rsa reboot-rsa2 + +install:: + @[ -d $(BASE)/lib/drivers ] || mkdir $(BASE)/lib/drivers + for f in $(DRIVERS); do \ + rm -f $(BASE)/lib/drivers/$$f; \ + cp -p drivers/$$f $(BASE)/lib/drivers/$$f; \ + chmod 755 $(BASE)/lib/drivers/$$f; \ + done diff --git a/conmux/drivers/reboot-netfinity b/conmux/drivers/reboot-netfinity new file mode 100755 index 00000000..e7f986c1 --- /dev/null +++ b/conmux/drivers/reboot-netfinity @@ -0,0 +1,98 @@ +#!/usr/bin/expect -f +# +# reboot-netfinity -- reboot netfinity machines via their management port +# +# This expect script reboots a machine via a managemant port. +# It only works with the Netfinity-style Service Processors. +# They have a number-driven interface +# +# example machines: Netfinity 6000R, 8500R +# xSeries x350, x370 +# +# usage: +# reboot-netfinity <user> <password> <connect command> ... +# +# examples: +# reboot-netfinity USERID PASSW0RD telnet 1.2.3.4 7033 +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Dave Hansen <haveblue@us.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +set P "reboot-netfinity" + +if {[llength $argv] < 2} { + puts stderr "Usage: $P <userid> <password> <cmd> ..." + exit 1 +} +set argc [llength $argv] +set userid [lindex $argv 0] +set password [lindex $argv 1] +set timeout 30 +set send_slow {1 .1} + +set command [lrange $argv 2 end] + +puts "$P: Logging into service processor with command \"$command\" to restart it"; + +eval spawn [lrange $argv 2 end] + +send -s -- "\033\033\033\033\033" +expect *; +send "\033" + +expect { + #Log in + "USER ID:" { + send -s -- "$userid\r" + expect -exact "PASSWORD:" + send -s -- "$password\r" + exp_continue; + } + #Some machines use different login prompts + "User_id:" { + send -s -- "$userid\r" + expect -exact "Password:" + send -s -- "$password\r" + exp_continue; + } + #already logged in + "Z - Start Remote Video" { + send "6" + expect -exact "4 - Power On" + send "3" + expect -exact "2 - Power Off Immediately" + send "2" + expect -exact "0 - Write" + send "0" + expect -exact "6.3.2: Write" + + send -s -- "\033\033\033\033\033" + expect -exact "Z - Start Remote Video" + send "6" + expect -exact "4 - Power On" + send "4" + expect -exact "2 - Power On Release CPU's Reset" + send "2" + expect -exact "0 - Write" + send "0" + expect -exact "6.4.2: Write" + + # On kernels with ACPI support, the above may send an ACPI + # event to the OS to perform a clean shutdown. If that happens + # we may not ever come back up. Add the reboot command below + # to finish the job. + send -s -- "\033\033\033\033\033" + expect -exact "Z - Start Remote Video" + send "7" + expect -exact "4 - Restart SP" + send "2" + expect -exact "0 - Write" + send "0" + expect -exact "7.2: Write" + } + +} + +puts "$P: reboot initiated" diff --git a/conmux/drivers/reboot-newisys b/conmux/drivers/reboot-newisys new file mode 100755 index 00000000..a4080f59 --- /dev/null +++ b/conmux/drivers/reboot-newisys @@ -0,0 +1,49 @@ +#!/usr/bin/expect -f +# +# reboot-newisys -- reboot a newisys system via its management interface +# +# This expect script reboots a newisys machine using its command +# interface. This is typically accessed over ssh. +# +# usage: +# reboot-newisys <user> <password> <connect command> ... +# +# examples: +# reboot-newisys USERID PASSW0RD ssh 1.2.3.4 +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Daniel Sauble <djsauble@us.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +set P "reboot-newisys" + +if {[llength $argv] != 4} { + puts stderr "Usage: $P <userid> <password> <type> <machine>" + exit 1 +} +set userid [lindex $argv 0] +set password [lindex $argv 1] +set machine [lindex $argv 3] +set timeout 30 + +puts "$P: connecting to $machine to initiate reboot ..." + +# Log in +eval spawn "ssh -o StrictHostKeyChecking=no $userid\@$machine" +expect "password: " +send -- "$password\r" +expect "localhost \$ " +send "platform get power state\r" + +# Check to see if the machine is on or off +# then restart it +expect { + "On" {send "platform set power state cycle -f\r"} + "Off" {send "platform set power state on -f\r"} +} +expect "localhost \$ " +expect "localhost \$ " +send "exit\r" + +puts "$P: reboot initated" diff --git a/conmux/drivers/reboot-numaq b/conmux/drivers/reboot-numaq new file mode 100755 index 00000000..acabf1ec --- /dev/null +++ b/conmux/drivers/reboot-numaq @@ -0,0 +1,224 @@ +#!/usr/bin/expect -- +# +# reboot-numaq -- reboot Numa-Q systems connected to a VCS console +# +# Use the remote telnet managment interface on the VCS console to +# hardreset systems. Systems are identified by their system names +# within VCS. +# +# usage: +# reboot-numaq <type> <console IP> <system> <port> <userid> <password> +# +# examples: +# reboot-numaq vcs 1.2.3.4 hummer 12346 FOO BAR +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +set P "reboot-numaq" + +if { [llength $argv] != 6 } { + puts stderr "Usage: reboot-numaq vcs <console addr> <system> <console port> <userid> <password>" + exit 1 +} + +log_user 0 +#stty echo +set argc [llength $argv] +set console_ip [lindex $argv 1] +set system [lindex $argv 2] +set console_port [lindex $argv 3] +set username [lindex $argv 4] +set password [lindex $argv 5] + +#log_file -a "$logfile" + +set elapsed_time 0 +set timeout 30 + +set command "telnet $console_ip $console_port" + +proc note {m} { + global P + puts "$P: $m" +} +proc warn {m} { + global P + puts "$P: WARNING: $m" +} +proc winge {m} { + global P + puts "$P: MACHINE ERROR: reboot failed - $m" +} + +# CONNECT: connect to the remote console. +note "Logging into VCS console with command \"$command\" to restart it" +eval spawn $command +expect { + default { + winge "login prompt not issued" + exit 2 + } + "Connection closed by foreign host." { + winge "Telnet connection closed." + exit 1 + } + "Unable to connect to remote host:" { + winge "Connection to remote console failed"; + exit 2 + } + "login:" { + note "saw login prompt" + } +} + +# AUTHENTICATE: send username and password at the relevant prompts +note "sending login ..." +send -- "$username\r" +expect { + default { + winge "password prompt not issued" + exit 2 + } + "password:" { + note "password prompt found" + } +} + +note "sending password ..." +send -- "$password\r" +expect { + default { + winge "command prompt not issued" + exit 2 + } + "Authentication Failed" { + winge "login/password incorrect ... aborting" + exit 1 + } + -- "->" { + #note "command prompt found" + } +} + +# SYSTEM: try and select the system, if the specified system does not +# exist get a listing and print that out whilst we are connected. +note "selecting system '$system' ..." +send "cd $system\r" +set found 1 +expect { + default { + winge "command prompt not issued" + exit 2 + } + -re "cd: path '.*' not found" { + winge "system not defined" + set found 0 + exp_continue + } + -- "->" { + #note "command prompt found" + } +} + +# The system the user specified was not found, give them a hand by +# getting a list of systems. +if {$found == 0} { + note "Defined systems:" + send "sysdef -l\r" + expect { + default { + winge "command prompt not issued" + exit 2 + } + -re "sysdef -l\r\n$" { + exp_continue + } + -- "->" { + #note "command prompt found" + } + "\n$" { + puts -nonewline "$expect_out(buffer)" + exp_continue + } + } + note "complete ... exiting" + send "exit\r" + exit 1 +} + +# DOWN: shut the system down ... hard. Expect to see nothing before the +# prompt in the case of success. +set timeout 65 +set fail 1 +for {set retry 1} {$fail == 1 && $retry <= 3} {incr retry} { + note "powering off the system ..." + send "power -f -t 60000 off\r" + set fail 0 + expect { + default { + winge "command prompt not issued" + exit 2 + } + -- "->" { + #note "command prompt found" + } + "*E*" { + warn "power off failed ... attempt $retry" + puts -nonewline "$expect_out(buffer)" + set fail 1 + exp_continue + } + } +} +if {$fail == 1} { + winge "power off failed ... system not rebooted" + exit 2 +} +set timeout 30 + +# UP: power the system on ... and then ask it to boot. +note "powering on the system ..." +send "power on\r" +set fail 0 +expect { + default { + winge "command prompt not issued" + exit 2 + } + -- "->" { + #note "command prompt found" + } + "*E*" { + warn "power on failed ..." + set fail 1 + exp_continue + } +} +if {$fail == 1} { + winge "power off failed ... system not rebooted" + exit 2 +} + +note "booting the system ..." +send "boot\r" +expect { + default { + winge "command prompt not issued" + exit 2 + } + -- "->" { + #note "command prompt found" + } + "*E*" { + warn "boot failed ..." + set fail 1 + exp_continue + } +} + +note "complete ... exiting" +send "exit" +exit 0 diff --git a/conmux/drivers/reboot-rsa b/conmux/drivers/reboot-rsa new file mode 100755 index 00000000..9b5082db --- /dev/null +++ b/conmux/drivers/reboot-rsa @@ -0,0 +1,101 @@ +#!/usr/bin/expect +# +# reboot-rsa -- reboot systems with RSA management cards +# +# Reboot systems via their RSA (I) managment card interface. +# +# usage: +# reboot-rsa <userid> <password> <connect commands> ... +# +# example +# reboot-rsa FOO BAR telnet 1.2.3.4 +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Dave Hansen <haveblue@us.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +set P "reboot-rsa" + +if {[llength $argv] < 3} { + puts stderr "Usage: $P <username> <password> <cmd> ..." + exit 1 +} +set username [lindex $argv 0] +set password [lindex $argv 1] + +log_user 0 +#stty echo +#log_file -a "$logfile" + +proc note {m} { + global P + puts "$P: $m" +} +proc warn {m} { + global P + puts "$P: WARNING: $m" +} +proc winge {m} { + global P + puts "$P: ERROR: $m" +} + +set elapsed_time 0 +set timeout 10 + +set command [lrange $argv 2 end] +eval spawn [lrange $argv 2 end] + +note "Logging into service processor with command \"$command\" to restart it"; + +expect { + "Connection closed by foreign host." { + winge "Telnet connection closed." + exit 1; + } + "Unable to connect to remote host:" { + winge "Someone may already have the service processor"; + exit 2; + } + "Login ID:" { + send "$username\t$password\r" + } + timeout { + winge "never saw opening screen with \"Login ID:\"" + exit 2; + } +} + +expect { + "Log Off" { + send "\t\t\t\t\r" + note "Saw opening screen, selecting \"Server Power/Restart\""; + } + timeout { + } +} + +expect { + "Restart Server Immediately" { + note "Saw \"Server Power/Restart\" screen. Selecting \"Restart\""; + send "\t\t\t\t\t\r"; + } +} + +expect { + "Restarting the system immediately" { + note "Saw restart confirmation prompt, pressing enter"; + send "\r" + } +} + +expect { + "The system is performing a Restart Server Immediately" { + note "the system is restarting"; + exit 0; + } +} + +winge "an error occurred while restarting the server" +exit 1; diff --git a/conmux/drivers/reboot-rsa2 b/conmux/drivers/reboot-rsa2 new file mode 100755 index 00000000..b3d97b03 --- /dev/null +++ b/conmux/drivers/reboot-rsa2 @@ -0,0 +1,91 @@ +#!/usr/bin/expect +# +# reboot-rsa2 -- reboot systems via their RSA-II management cards +# +# Use a systems RSA (II) managment card to hard reset that system. +# +# usage: +# reboot-rsa2 <userid> <password> <connect command> ... +# +# examples: +# reboot-rsa2 FOO BAR telnet 1.2.3.4 23 +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Aileen Sheedy <asheedy@us.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +set P "reboot-rsa2" + +if {[llength $argv] < 3} { + puts stderr "Usage: $P <username> <password> <cmd> ..." + exit 1 +} +set username [lindex $argv 0] +set password [lindex $argv 1] + +log_user 0 +#stty echo +#log_file -a "$logfile" + +proc note {m} { + global P + puts "$P: $m" +} +proc warn {m} { + global P + puts "$P: WARNING: $m" +} +proc winge {m} { + global P + puts "$P: ERROR: $m" +} + +set elapsed_time 0 +set timeout 10 + +set command [lrange $argv 2 end] +eval spawn [lrange $argv 2 end] + +note "Logging into service processor with command \"$command\" to restart it" + +expect { + "Connection closed by foreign host." { + winge "Telnet connection closed." + exit 1 + } + "Unable to connect to remote host:" { + winge "Someone may already have the service processor" + exit 2 + } + "username:" { + send "$username\r$password\r" + } + timeout { + winge "Never saw opening screen with \"username:\"" + exit 2 + } +} + +expect { + "Invalid login!" { + winge "Invalid username or password" + exit 2 + } + ">" { + send "reset\r" + } +} + +expect { + "ok" { + note "System restarting" + send "exit\r" + exit 0 + } + timeout { + } +} + +winge "an error occurred while restarting the server" +exit 1 diff --git a/conmux/examples/README b/conmux/examples/README new file mode 100755 index 00000000..7b12b429 --- /dev/null +++ b/conmux/examples/README @@ -0,0 +1,11 @@ +EXAMPLES +======== +This directory contains some sample configuration files for conmux. + +socket.cf -- where the console is exposed directly on a tcp port on + the local network. + +command.cf -- where the console is obtained through a driver. + +Details on the command line options for each driver are in the +comments at the top of the driver. diff --git a/conmux/examples/command.cf b/conmux/examples/command.cf new file mode 100755 index 00000000..1d6b9990 --- /dev/null +++ b/conmux/examples/command.cf @@ -0,0 +1,4 @@ +listener ppc64 +application console 'ppc64 console' 'hmc open-term -m bigppc -p bigppc-lpar1 -U user -P passwd -V 2.6 hmc 1.2.3.4' + command 'hardreset' 'initated a hard reset' 'hmc reboot -m bigppc -p bigppc-lpar1 -U user -P passwd -V 2.6 hmc 1.2.3.4' + help 'hardreset' 'initiate a system hard reset for this machine' diff --git a/conmux/examples/socket.cf b/conmux/examples/socket.cf new file mode 100755 index 00000000..513622d3 --- /dev/null +++ b/conmux/examples/socket.cf @@ -0,0 +1,4 @@ +listener hummer +socket console 'hummer console' '1.2.3.4:2088' + command 'hardreset' 'initated a hard reset' 'reboot-numaq vcs 1.2.3.5 hummer 12346 user passwd' + help 'hardreset' 'initiate a system hard reset for this machine' diff --git a/conmux/helpers/autoboot-helper b/conmux/helpers/autoboot-helper new file mode 100755 index 00000000..e147fc82 --- /dev/null +++ b/conmux/helpers/autoboot-helper @@ -0,0 +1,72 @@ +#! /usr/bin/expect +# +# autoboot-helper -- automatic boot helper +# +# Some machines have real issues rebooting. This helper watches their +# console output for the telltale signs of a reboot in progress. When +# spotted this triggers an automated 'manual' hardreset. For use when +# machines fail to reboot at the BIOS level. +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +set P "autoboot-numaq" +log_user 0 + +if {$argc != 0} { + puts stderr "Usage: $P" + exit 1 +} + +proc note {msg} { + global P + puts stderr "$P: $msg" +} +proc warn {msg} { + global P + puts stderr "$P: $msg" + puts "~\$msg $msg" +} + +set timeout -1 +set likely 0 +expect_user { + {TEST;} { + warn "test trigger detected" + exp_continue + } + {Unmounting file systems} { + note "controlled reboot in progress ..." + set likely [clock seconds] + exp_continue + } + {Unmounting local filesystems...} { + note "controlled reboot in progress ..." + set likely [clock seconds] + exp_continue + } + -ex {***** REBOOT LINUX *****} { + note "fsck failure occured ..." + set likely [clock seconds] + exp_continue + } + {Restarting system.} { + if {$likely != 0} { + warn "shutdown complete, restart indicated" + puts "~\$hardreset" + set likely 0 + } else { + warn "likely false positive" + } + exp_continue + } + "*\n" { + if {$likely > 0 && ([clock seconds] - $likely) > 60} { + warn "trigger timeout" + set likely 0 + } + exp_continue + } +} diff --git a/conmux/helpers/module.mk b/conmux/helpers/module.mk new file mode 100755 index 00000000..649f2ef3 --- /dev/null +++ b/conmux/helpers/module.mk @@ -0,0 +1,14 @@ +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 + +HELPERS:=autoboot-helper tickle-helper + +install:: + @[ -d $(BASE)/lib/helpers ] || mkdir $(BASE)/lib/helpers + for f in $(HELPERS); do \ + rm -f $(BASE)/lib/helpers/$$f; \ + cp -p helpers/$$f $(BASE)/lib/helpers/$$f; \ + chmod 755 $(BASE)/lib/helpers/$$f; \ + done diff --git a/conmux/helpers/tickle-helper b/conmux/helpers/tickle-helper new file mode 100755 index 00000000..2623c529 --- /dev/null +++ b/conmux/helpers/tickle-helper @@ -0,0 +1,99 @@ +#! /usr/bin/expect +# +# tickle-helper -- watch for reboots and 'tickle' the console during them +# +# Some consoles get broken when the machine reboots. There are normally +# fixed by trying to use them at or arround the reboot. Watch for reboots +# and initiate use of the console to trigger a drop/reconnect cycle. +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +set P "tickle-helper" +log_user 0 + +if {$argc != 0} { + puts stderr "Usage: $P" + exit 1 +} + +proc note {msg} { + global P + puts stderr "$P: $msg" +} +proc warn {msg} { + global P + puts stderr "$P: $msg" + puts "~\$msg $msg" +} + +proc tickle {} { + set timeout 5 + warn "tickling console ..." + puts "" + set now [clock seconds] + expect_user { + {blade: ERROR: console lost} { + } + {Elapsed time since release of system processors:} { + } + "*\n" { + if {([clock seconds] - $now) > 5} { + set now [clock seconds] + warn "tickling console ..." + puts "" + } + exp_continue + } + timeout { + set now [clock seconds] + warn "tickling console ..." + puts "" + exp_continue + } + } + set timeout -1 + warn "tickle complete ..." +} + +set timeout -1 +set likely 0 +expect_user { + {TEST;} { + warn "test trigger detected" + exp_continue + } + -re {Unmounting file systems|Unmounting local filesystems...} { + note "controlled reboot in progress ..." + set likely [clock seconds] + exp_continue + } + -ex {***** REBOOT LINUX *****} { + note "fsck failure occured ..." + set likely [clock seconds] + exp_continue + } + -re {HARDBOOT INITIATED|initated a hard reset} { + tickle + exp_continue + } + -re {Please stand by while rebooting the system|Restarting system} { + if {$likely != 0} { + warn "shutdown complete, restart indicated" + tickle + set likely 0 + } else { + warn "likely false positive" + } + exp_continue + } + "*\n" { + if {$likely > 0 && ([clock seconds] - $likely) > 60} { + warn "trigger timeout" + set likely 0 + } + exp_continue + } +} diff --git a/conmux/mkdeb b/conmux/mkdeb new file mode 100755 index 00000000..14c43ef0 --- /dev/null +++ b/conmux/mkdeb @@ -0,0 +1,9 @@ +#!/bin/sh +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +read name major minor patch build <.version +alien --version=$major.$minor.$patch --description="console multiplexor" conmux.tgz diff --git a/conmux/start b/conmux/start new file mode 100755 index 00000000..92402af6 --- /dev/null +++ b/conmux/start @@ -0,0 +1,138 @@ +#! /bin/sh +# +# start -- start up configured conmux servers on this host. +# +# (C) Copyright IBM Corp. 2004, 2005, 2006 +# Author: Andy Whitcroft <andyw@uk.ibm.com> +# +# The Console Multiplexor is released under the GNU Public License V2 +# +if [ -f ~/.gmm.conf ]; then + . ~/.gmm.conf +fi +CONMUX=${CONMUX:-/usr/local/conmux} + +cmd="start" +if [ "$1" != "" ]; then + cmd="$1" +fi + +PATH=$CONMUX/bin:$CONMUX/sbin:$CONMUX/lib/drivers:$CONMUX/lib/helpers:$PATH + +function start() { + typeset name="$1" + typeset pf="$CONMUX/log/$name.pid" + + shift + + # Determine whether it is already running ... if so leave it be. + if [ -f "$pf" ]; then + if kill -0 `cat "$pf"` 2>/dev/null; then + return 1 + fi + fi + + echo "starting $name ..." + "$@" >"$CONMUX/log/$name.log" 2>&1 & + echo "$!" >"$pf" + + return 0 +} +function stop() { + typeset name="$1" + typeset pf="$CONMUX/log/$name.pid" + + echo "stopping $name ..." + # Kill it and clear up + kill -HUP `cat "$pf"` 2>/dev/null + rm -f "$pf" +} + +existing="" +for i in $CONMUX/log/*.pid +do + n=${i%.pid} + n=${n#$CONMUX/log/} + + if [ "$n" != "*" ]; then + existing="$existing $n" + fi +done + +if [ "$cmd" = "start" ]; then + autoboot="" + [ -f $CONMUX/etc/registry ] || touch $CONMUX/etc/registry + start registry $CONMUX/sbin/conmux-registry 63000 $CONMUX/etc/registry + if [ "$?" -eq 0 ]; then + sleep 1 + fi + started="registry" + pause=0 + for i in $CONMUX/etc/*.cf + do + n=${i%.cf} + n=${n#$CONMUX/etc/} + + if [ "$n" != "*" ]; then + if [ -f "$CONMUX/log/$n.cf" ]; then + if ! cmp -s "$i" "$CONMUX/log/$n.cf"; then + stop $n + fi + fi + start $n $CONMUX/sbin/conmux $i + if [ "$?" -eq 0 ]; then + pause=1 + fi + started="$started $n" + + # Preserve the orginal configuration file. + cp "$i" "$CONMUX/log/$n.cf" + + if grep -q TYPE:numaq "$i"; then + autoboot="$autoboot $n" + fi + for i in `grep FLAGS: "$i"`; do + case "$i" in + \#|FLAGS:) ;; + *) autoboot="$autoboot $n/$i" ;; + esac + done + fi + done + if [ "$pause" -eq 1 ]; then + sleep 1 + fi + for nh in $autoboot + do + name="${nh%/*}" + helper="${nh#*/}" + + mn="${name#abat-}" + start $name-$helper-helper $CONMUX/bin/conmux-attach $mn $helper-helper + started="$started $name-$helper-helper" + done +fi + +if [ "$cmd" = "start" -o "$cmd" = "stop" ]; then + for i in $existing + do + case " $started " in + *\ $i\ *) ;; + *) stop "$i" ;; + esac + done +fi + +if [ "$cmd" = "status" ]; then + for n in $existing + do + mn="${n#abat-}" + case "$n" in + registry|*-helper) + ;; + *) + status=`console -s $mn` + echo "$mn $status" + esac + done +fi |