summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/main/java/org/openslx/satserver/util/MessageSink.java8
-rw-r--r--src/main/java/org/openslx/satserver/util/WakeOnLanExecutor.java136
-rw-r--r--src/main/java/org/openslx/taskmanager/tasks/RemoteExec.java36
-rw-r--r--src/main/java/org/openslx/taskmanager/tasks/WakeOnLan.java281
4 files changed, 345 insertions, 116 deletions
diff --git a/src/main/java/org/openslx/satserver/util/MessageSink.java b/src/main/java/org/openslx/satserver/util/MessageSink.java
new file mode 100644
index 0000000..bda7410
--- /dev/null
+++ b/src/main/java/org/openslx/satserver/util/MessageSink.java
@@ -0,0 +1,8 @@
+package org.openslx.satserver.util;
+
+public interface MessageSink
+{
+
+ public void addMsg( String str );
+
+}
diff --git a/src/main/java/org/openslx/satserver/util/WakeOnLanExecutor.java b/src/main/java/org/openslx/satserver/util/WakeOnLanExecutor.java
new file mode 100644
index 0000000..1ee2f81
--- /dev/null
+++ b/src/main/java/org/openslx/satserver/util/WakeOnLanExecutor.java
@@ -0,0 +1,136 @@
+package org.openslx.satserver.util;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class WakeOnLanExecutor
+{
+
+ private static final Pattern RE_IPv4 = Pattern.compile( "^(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)$" );
+ private static final Pattern RE_MAC = Pattern.compile( "^([0-9a-f]{2})[:-]([0-9a-f]{2}+)[:-]([0-9a-f]{2}+)[:-]([0-9a-f]{2}+)[:-]([0-9a-f]{2}+)[:-]([0-9a-f]{2}+)$",
+ Pattern.CASE_INSENSITIVE );
+
+ private byte[] buffer;
+
+ public WakeOnLanExecutor( String password ) throws RuntimeException
+ {
+ Matcher m = null;
+ int pwlen = 0;
+ if ( !Util.isEmpty( password ) ) {
+ m = RE_IPv4.matcher( password );
+ if ( m.matches() ) {
+ pwlen = 4;
+ } else {
+ m = RE_MAC.matcher( password );
+ if ( m.matches() ) {
+ pwlen = 6;
+ }
+ }
+ if ( pwlen == 0 ) {
+ throw new RuntimeException( "Invalid password format: " + password );
+ }
+ }
+ buffer = new byte[ 17 * 6 + pwlen ];
+ // 6 Sync bytes
+ for ( int i = 0; i < 6; ++i ) {
+ buffer[i] = -1;
+ }
+ if ( pwlen != 0 ) {
+ // Append pw
+ try {
+ for ( int i = 0; i < pwlen; ++i ) {
+ int x = Integer.parseInt( m.group( i + 1 ), pwlen == 4 ? 10 : 16 );
+ buffer[17 * 6 + i] = (byte)x;
+ }
+ } catch ( Throwable t ) {
+ throw new RuntimeException( "Invalid octet in password" );
+ }
+ }
+ }
+
+ /**
+ * Send out a bunch of WOL packets to the given list of MAC addresses. Every packet will be
+ * directed to given ip, i.e. all destination MACs have to be in the same subnet.
+ *
+ * @param log For adding log outout
+ * @param macs list of MAC addresses
+ * @param ip destination IP (broadcast or directed broadcast address)
+ * @param port destination port, usually 9
+ * @return list of MACs the packet has successfully been sent to
+ */
+ public String[] execute( MessageSink log, Collection<String> macs, String ip, int port )
+ {
+ if ( port == 0 ) {
+ port = 9;
+ } else if ( port > 65535 || port < 0 ) {
+ log.addMsg( "Invalid port: " + port + " for " + ip );
+ return new String[ 0 ];
+ }
+
+ DatagramSocket sock = null;
+ InetAddress destAddr = null;
+ Set<String> success = new HashSet<>();
+ try {
+ try {
+ destAddr = InetAddress.getByName( ip );
+ sock = new DatagramSocket();
+ sock.setBroadcast( true );
+ } catch ( SocketException e ) {
+ log.addMsg( "Cannot create UDP socket" );
+ return new String[ 0 ];
+ } catch ( UnknownHostException e ) {
+ log.addMsg( "Cannot resolve " + ip );
+ return new String[ 0 ];
+ }
+
+ // Repeat three times
+ for ( int reps = 0; reps < 3; ++reps ) {
+ if ( reps != 0 ) {
+ Thread.sleep( 400 );
+ }
+ // For each MAC, send WOL packet
+ for ( String mac : macs ) {
+ Matcher m = RE_MAC.matcher( mac );
+ if ( !m.matches() ) {
+ log.addMsg( "Cannot parse MAC address " + mac );
+ continue;
+ }
+ try {
+ // Patch in the 16 repetitions of the target mac address
+ for ( int i = 0; i < 6; ++i ) {
+ byte x = (byte)Integer.parseInt( m.group( i + 1 ), 16 );
+ for ( int offset = 0; offset < 16; ++offset ) {
+ buffer[6 + offset * 6 + i] = x;
+ }
+ }
+ DatagramPacket dp = new DatagramPacket( buffer, buffer.length, destAddr, port );
+ sock.send( dp );
+ log.addMsg( "Sent packet to " + mac );
+ success.add( mac );
+ Thread.sleep( 10 );
+ } catch ( NumberFormatException e ) {
+ log.addMsg( "Invalid octet in MAC address, skipping: " + mac );
+ } catch ( IOException e ) {
+ log.addMsg( "Error sending UDP packet to " + ip + "/" + mac + ": " + e.toString() );
+ }
+ }
+ }
+ } catch ( InterruptedException t ) {
+ Thread.currentThread().interrupt();
+ } finally {
+ Util.multiClose( sock );
+ }
+
+ return success.toArray( new String[ success.size() ] );
+ }
+
+}
diff --git a/src/main/java/org/openslx/taskmanager/tasks/RemoteExec.java b/src/main/java/org/openslx/taskmanager/tasks/RemoteExec.java
index de520a8..c533c27 100644
--- a/src/main/java/org/openslx/taskmanager/tasks/RemoteExec.java
+++ b/src/main/java/org/openslx/taskmanager/tasks/RemoteExec.java
@@ -50,6 +50,19 @@ public class RemoteExec extends AbstractTask
private JSch sshClient;
private Output status = new Output();
+
+ public RemoteExec()
+ {
+ }
+
+ public RemoteExec(Client[] clients, String sshkey, int port, String command, int timeoutSeconds)
+ {
+ this.clients = clients;
+ this.sshkey = sshkey;
+ this.port = port;
+ this.command = command;
+ this.timeoutSeconds = timeoutSeconds;
+ }
@Override
protected boolean initTask()
@@ -160,7 +173,12 @@ public class RemoteExec extends AbstractTask
return true;
}
-
+
+ public Output getStatusObject()
+ {
+ return this.status;
+ }
+
private void execCommand( ChannelExec channel, Client client, Result result ) throws JSchException, IOException
{
long st = System.currentTimeMillis();
@@ -279,9 +297,9 @@ public class RemoteExec extends AbstractTask
static class Output
{
/** UUID -> Output */
- private Map<String, Result> result = new ConcurrentHashMap<>();
+ Map<String, Result> result = new ConcurrentHashMap<>();
- private String error;
+ String error;
private synchronized void addError( String e )
{
@@ -305,6 +323,18 @@ public class RemoteExec extends AbstractTask
private String username;
/** How many ms of the given timeout are left */
private int timeoutLeft;
+
+ public Client()
+ {
+ }
+
+ public Client(String machineuuid, String clientip, int port, String username)
+ {
+ this.machineuuid = machineuuid;
+ this.clientip = clientip;
+ this.port = port;
+ this.username = username;
+ }
}
static enum State
diff --git a/src/main/java/org/openslx/taskmanager/tasks/WakeOnLan.java b/src/main/java/org/openslx/taskmanager/tasks/WakeOnLan.java
index 246749a..c92f3c9 100644
--- a/src/main/java/org/openslx/taskmanager/tasks/WakeOnLan.java
+++ b/src/main/java/org/openslx/taskmanager/tasks/WakeOnLan.java
@@ -1,163 +1,218 @@
package org.openslx.taskmanager.tasks;
-import java.io.IOException;
-import java.net.DatagramPacket;
-import java.net.DatagramSocket;
-import java.net.InetAddress;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import org.apache.logging.log4j.util.Strings;
+import org.openslx.satserver.util.MessageSink;
import org.openslx.satserver.util.Util;
+import org.openslx.satserver.util.WakeOnLanExecutor;
import org.openslx.taskmanager.api.AbstractTask;
+import org.openslx.taskmanager.tasks.RemoteExec.Output;
import com.google.gson.annotations.Expose;
public class WakeOnLan extends AbstractTask
{
- private static final Pattern RE_IPv4 =
- Pattern.compile( "^(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)$" );
- private static final Pattern RE_MAC =
- Pattern.compile( "^([0-9a-f]{2})[:-]([0-9a-f]{2}+)[:-]([0-9a-f]{2}+)[:-]([0-9a-f]{2}+)[:-]([0-9a-f]{2}+)[:-]([0-9a-f]{2}+)$",
- Pattern.CASE_INSENSITIVE );
-
+ /**
+ * List of clients to wake up
+ */
@Expose
- private String[] macs;
+ private List<Machine> clients;
+ /**
+ * SHH data (key, command) for waking via another host
+ */
@Expose
- private String ip;
+ private Map<String, SshData> ssh;
- @Expose
- private String password;
-
+ /**
+ * UDP port to send WOL packets to
+ */
@Expose
private int port;
private StatusObject status;
- private byte[] buffer;
-
@Override
protected boolean initTask()
{
status = new StatusObject();
this.setStatusObject( status );
- if ( macs == null || macs.length == 0 ) {
- status.addMsg( "Macs empty" );
- }
- if ( Util.isEmpty( ip ) ) {
- status.addMsg( "IP empty" );
- }
- int pwlen = 0;
- Matcher m = null;
- if ( !Util.isEmpty( password ) ) {
- m = RE_IPv4.matcher( password );
- if ( m.matches() ) {
- pwlen = 4;
- } else {
- m = RE_MAC.matcher( password );
- if ( m.matches() ) {
- pwlen = 6;
+ if ( clients == null ) {
+ status.addMsg( "Clients null" );
+ } else {
+ // Clean
+ for ( Iterator<Machine> it = this.clients.iterator(); it.hasNext(); ) {
+ Machine client = it.next();
+ if ( client == null || Util.isEmpty( client.mac ) || Util.isEmpty( client.ip )
+ || client.methods == null || client.methods.isEmpty() ) {
+ it.remove();
}
}
- if ( pwlen == 0 ) {
- status.addMsg( "Invalid password format: " + password );
+ if ( clients.isEmpty() ) {
+ status.addMsg( "Clients empty" );
}
}
if ( !Util.isEmpty( status.messages ) )
return false;
- buffer = new byte[ 17 * 6 + pwlen ];
- if ( pwlen != 0 ) {
- try {
- for ( int i = 0; i < pwlen; ++i ) {
- int x = Integer.parseInt( m.group( i + 1 ), pwlen == 4 ? 10 : 16 );
- buffer[17 * 6 + i] = (byte)x;
- }
- } catch ( Throwable t ) {
- status.addMsg( "Invalid octet in password" );
- return false;
- }
- }
- if ( this.port == 0 ) {
- this.port = 9;
- } else if ( this.port > 65535 || this.port < 0 ) {
- status.addMsg( "Invalid port: " + this.port );
- return false;
- }
return true;
}
@Override
protected boolean execute()
{
- DatagramSocket sock = null;
- try {
- try {
- sock = new DatagramSocket();
- sock.setBroadcast( true );
- } catch ( SocketException e ) {
- status.addMsg( "Cannot create UDP socket" );
- return false;
- }
-
- // Sync bytes
- for ( int i = 0; i < 6; ++i ) {
- buffer[i] = -1;
- }
-
- // Repeat three times
- for ( int reps = 0; reps < 3; ++reps ) {
- if ( reps != 0 ) {
- try {
- Thread.sleep( 600 );
- } catch ( InterruptedException t ) {
- Thread.currentThread().interrupt();
- break;
- }
+ // Loop over clients until they all were handled (i.e. methods is empty)
+ ExecutorService tp = Executors.newFixedThreadPool( ssh.size() > 4 ? 4 : ssh.size() );
+ Map<String, ArrayList<String>> byMethod;
+ do {
+ byMethod = new HashMap<>();
+ // Fetch next method for all clients
+ for ( Machine client : clients ) {
+ if ( client.methods.isEmpty() )
+ continue;
+ // Group by method and destination IP address
+ String method = client.methods.remove( 0 ) + "//" + client.ip;
+ ArrayList<String> list = byMethod.get( method );
+ if ( list == null ) {
+ list = new ArrayList<>();
+ byMethod.put( method, list );
}
- // For each MAC
- for ( int mi = 0; mi < macs.length; ++mi ) {
- Matcher m = RE_MAC.matcher( macs[mi] );
- if ( !m.matches() ) {
- status.addMsg( "Cannot parse MAC address " + macs[mi] );
- continue;
- }
- try {
- for ( int i = 0; i < 6; ++i ) {
- byte x = (byte)Integer.parseInt( m.group( i + 1 ), 16 );
- for ( int offset = 0; offset < 16; ++offset ) {
- buffer[6 + offset * 6 + i] = x;
+ list.add( client.mac );
+ }
+ // Execute
+ List<Future<?>> waitList = new ArrayList<>();
+ for ( Entry<String, ArrayList<String>> it : byMethod.entrySet() ) {
+ String[] parts = it.getKey().split( "//" );
+ String method = parts[0];
+ String ip = parts[1];
+ if ( method.equalsIgnoreCase( "DIRECT" ) ) {
+ // Directly from server
+ waitList.add( tp.submit( () -> {
+ WakeOnLanExecutor wol = new WakeOnLanExecutor( null );
+ String[] success = wol.execute( status, it.getValue(), ip, this.port );
+ markSuccess( Arrays.asList( success ) );
+ } ) );
+ } else if ( this.ssh.containsKey( method ) ) {
+ // Via SSH
+ waitList.add( tp.submit( () -> {
+ SshData sshData = this.ssh.get( method );
+ String macs = Strings.join( it.getValue().iterator(), ' ' );
+ String command = sshData.command.replace( "%MACS%", macs ).replace( "%IP%", ip );
+ RemoteExec.Client c = new RemoteExec.Client( "x", sshData.ip, sshData.port, sshData.username );
+ RemoteExec task = new RemoteExec( new RemoteExec.Client[] { c }, sshData.sshkey, sshData.port, command, 5 );
+ task.execute();
+ Output s = task.getStatusObject();
+ if ( s != null ) {
+ if ( s.result != null && s.result.containsKey( "x" ) && s.result.get( "x" ).exitCode == 0 ) {
+ markSuccess( Arrays.asList( macs ) );
+ }
+ if ( !Util.isEmpty( s.error ) ) {
+ status.addMsg( s.error );
}
}
- } catch ( NumberFormatException e ) {
- status.addMsg( "Invalid octet in MAC address: " + macs[mi] );
- continue;
- }
- DatagramPacket dp = new DatagramPacket( buffer, buffer.length, InetAddress.getByName( ip ), this.port );
- try {
- sock.send( dp );
- status.addMsg( "Sent packet to " + macs[mi] );
- } catch ( IOException e ) {
- status.addMsg( "Error sending UDP packet to " + ip + "/" + macs[mi] + ": " + e.toString() );
- }
+ } ) );
+ } else {
+ status.addMsg( "Ignoring unknown SSH method '" + method + "'" );
}
}
- } catch ( UnknownHostException e ) {
- status.addMsg( "Cannot resolve " + ip );
- return false;
- } finally {
- Util.multiClose( sock );
+ // Wait for all jobs
+ for ( Future<?> f : waitList ) {
+ try {
+ f.get();
+ } catch ( InterruptedException e ) {
+ status.addMsg( "Threadpool got interrupted" );
+ Thread.currentThread().interrupt();
+ return false;
+ } catch ( ExecutionException e ) {
+ status.addMsg( "A treadpool job threw an exception" );
+ e.printStackTrace();
+ }
+ }
+ } while ( !byMethod.isEmpty() );
+ if ( this.clients.isEmpty() )
+ return true;
+ status.addMsg( "Failed clients:" );
+ for ( Machine c : clients ) {
+ status.addMsg( c.ip );
}
- return true;
+ return false;
}
- class StatusObject
+ private void markSuccess( Collection<String> macs )
+ {
+ synchronized ( this.clients ) {
+ for ( Iterator<Machine> it = this.clients.iterator(); it.hasNext(); ) {
+ Machine client = it.next();
+ if ( macs.contains( client.mac ) ) {
+ it.remove();
+ }
+ }
+ }
+ }
+
+ static class Machine
+ {
+ /**
+ * Destination IP (for directed broadcast)
+ */
+ @Expose
+ String ip;
+ /**
+ * Destination MAC
+ */
+ @Expose
+ String mac;
+ /**
+ * Desired WOL modes, from most to least preferred. either a key to the top level ssh map,
+ * or "DIRECT" for sending a WOL packet directly from the server
+ */
+ @Expose
+ List<String> methods;
+ /**
+ * WOL password (currently unused by slx-admin)
+ */
+ @Expose
+ private String password;
+ }
+
+ static class SshData
+ {
+ @Expose
+ String username;
+ @Expose
+ String sshkey;
+ @Expose
+ String ip;
+ @Expose
+ int port;
+ @Expose
+ String command;
+ }
+
+ /*
+ *
+ */
+
+ /**
+ * Return any messages about progress and status
+ */
+ static class StatusObject implements MessageSink
{
private String messages = "";
- public void addMsg( String str )
+ public synchronized void addMsg( String str )
{
messages = messages + "\n" + str;
}