25 мая 2013 г.

Программное подключение по ssh при помощи jsch

Используются: Maven 3, IntelliJ Idea 12 Ultimate, JSch 0.1.50, SSH.

Рассмотрим программное взаимодействие с удалённым сервером по SSH. Для этого нам пригодится библиотека JSch. Для удобства создадим maven-проект в Idea (File - New Module - Maven Module). Назовём проект ssh-example. В качестве архетипа проекта можно использовать архетип по умолчанию (т.е. базовый пустой проект). В pom.xml добавим последнюю версию библиотеки:
<dependencies>
<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.50</version>
</dependency>
</dependencies>
Давайте создадим класс ru.blogspot.developer.remarks.SshManager. В него добавим статические поля, чтобы в любой момент эти параметры легко было найти и изменить.
private static final int SSH_PORT = 22;
private static final int CONNECTION_TIMEOUT = 10000;
private static final int BUFFER_SIZE = 1024;
SSH_PORT - это порт для SSH подключения. На любом сервере обычно это 22-ой порт. Однако может быть и другой. CONNECTION_TIMEOUT - время, в течение которого мы будем ожидать ответа от удалённого сервера. Указывается в миллисекундах. В данном случае у нас установлено 10 секунд. BUFFER_SIZE - размер буфера для приёма ответа от сервера.
Следующие три параметра нужны для доступа к удалённом хосту. Разумеется, у вас они будут другими.
private static final String HOSTNAME = "192.168.1.2";
private static final String USERNAME = "admin";
private static final String PASSWORD = "123456";
Далее статический метод main, создающий экземпляр нашего класса и вызывающий подключение к хосту. Затем мы выводим полученные строки на экран.
public static void main(String[] args) {
SshManager manager = new SshManager();
List<String> lines = manager.connectAndExecuteListCommand(HOSTNAME, USERNAME, PASSWORD);
System.out.println(lines);
}
Основной метод, в котором реализуется вся логика работы нашего приложения:
public List<String> connectAndExecuteListCommand(String host, String username, String password) {
List<String> lines = new ArrayList<String>();
try {
    String command = "ls -1\n";
    Session session = initSession(host, username, password);
    Channel channel = initChannel(command, session);
    InputStream in = channel.getInputStream();
    channel.connect();
    String dataFromChannel = getDataFromChannel(channel, in);
    lines.addAll(Arrays.asList(dataFromChannel.split("\n")));
    channel.disconnect();
    session.disconnect();
} catch (Exception e) {
    System.out.println(e);
}
return lines;
}
В качестве примера мы подключимся к хосту и выполним там команду ls -l (вывести все файлы и папки в текущей директории в один столбец). Результат выполнения мы получим в виде набора строк, по одному элементу в каждой строке. Разумеется, вы можете выполнить абсолютно любую консольную команду, какую вы захотите.

Далее идут вспомогательные методы. Метод создания сессии:
private Session initSession(String host, String username, String password) throws JSchException {
JSch jsch = new JSch();
Session session = jsch.getSession(username, host, SSH_PORT);
session.setPassword(password);
UserInfo userInfo = new MyUserInfo();
session.setUserInfo(userInfo);
session.setConfig("StrictHostKeyChecking", "no");
session.connect(CONNECTION_TIMEOUT);
return session;
}
Здесь нам потребуется создать вспомогательный класс MyUserInfo, реализующий интерфейс UserInfo.

public class MyUserInfo implements UserInfo {

    private String password;

    public void showMessage(String message) {
        System.out.println(message);
    }

    public boolean promptYesNo(String message) {
        System.out.println(message);
        return true;
    }

    @Override
    public String getPassphrase() {
        return null;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public boolean promptPassphrase(String arg0) {
        System.out.println(arg0);
        return true;
    }

    @Override
    public boolean promptPassword(String arg0) {
        System.out.println(arg0);
        this.password = arg0;
        return true;
    }
}

По сути это заранее определённый набор ответов пользователя на запросы системы. Например, при запросе пароля будет автоматически введён пароль, который здесь указан. При любом вопросе с вариантами ответа "Да/Нет" всегда будет подставляться ответ "Да". Такой класс позволяет полностью автоматизировать взаимодействие по SSH.

Вернёмся к нашему основному классу. Следующий вспомогательный метод:
private Channel initChannel(String commands, Session session) throws JSchException {
Channel channel = session.openChannel("exec");
ChannelExec channelExec = (ChannelExec) channel;
channelExec.setCommand(commands);
channelExec.setInputStream(null);
channelExec.setErrStream(System.err);
return channel;
}
Здесь создаётся канал для выполнения команд, о чём свидетельствует параметр "exec". Устанавливаем строку с командами, которые мы хотим выполнить. Если вы хотите выполнить сразу несколько команд, просто разделите их символом перевода строки, как и в обычной командной оболочке. Входной поток устанавливаем в null (мы его не будем использовать). Поток ошибок устанавливаем, соответственно, в System.err.

Метод для получения данных из канала (т.е. ответа от хоста).
private String getDataFromChannel(Channel channel, InputStream in)
    throws IOException {
StringBuilder result = new StringBuilder();
byte[] tmp = new byte[BUFFER_SIZE];
while (true) {
    while (in.available() > 0) {
        int i = in.read(tmp, 0, BUFFER_SIZE);
        if (i < 0) {
            break;
        }
        result.append(new String(tmp, 0, i));
    }
    if (channel.isClosed()) {
        int exitStatus = channel.getExitStatus();
        System.out.println("exit-status: " + exitStatus);
        break;
    }
    trySleep(1000);
}
return result.toString();
}
Здесь мы считываем данные в буфер указанного нами размера до тех пор, пока данные продолжают поступать. После закрытия канала получаем код статуса. Если всё прошло успешно, статус равен нулю. Пока канал не закрыт, ожидаем получения новых данных с интервалом в одну секунду.

Для этого используется ещё один простой метод:
private void trySleep(int sleepTimeInMilliseconds) {
try {
    Thread.sleep(sleepTimeInMilliseconds);
} catch (Exception e) {
}
}
Всё, наш основной класс готов. Осталась одна маленькая деталь.

Нам нужно создать автономный исполняемый (runnable jar) файл, чтобы его легко можно было запускать прямо из командной строки вне какой-либо среды исполнения. Как это сделать, описано здесь.

Теперь выполните в консоли команду java -jar target/ssh-example-1.0-SNAPSHOT-executable.jar, находясь при этом в директории вашего проекта. Если всё было сделано правильно, вы получите список файлов и каталогов в домашней директории на удалённом хосте.

24 мая 2013 г.

Создание runnable jar файла с помощью maven

Очень часто в целях тестирования требуется создать простое приложение, которое можно запускать вне какого-либо сервера приложений, просто по команде java -jar. Для этого нам нужно создать runnable jar файл, в который уже будут интегрированы все необходимые нам библиотеки. Чтобы не делать этого вручную, воспользуемся плагином maven-shade-plugin. В pom.xml добавим следующую секцию:

<build>
<plugins>
    <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
            <source>1.6</source>
            <target>1.6</target>
        </configuration>
    </plugin>
    <plugin>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.0</version>
        <executions>
            <execution>
                <phase>package</phase>
                <goals>
                    <goal>shade</goal>
                </goals>
                <configuration>
                    <transformers>
                        <transformer
                                implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                            <mainClass>ru.blogspot.developer.remarks.MainClass</mainClass>
                        </transformer>
                    </transformers>
                    <shadedArtifactAttached>true</shadedArtifactAttached>
                    <shadedClassifierName>executable</shadedClassifierName>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>
</build>

Можете копировать её целиком. Единственное, на что здесь следует обратить внимание - это секция transformers. В ней вы можете указывать все необходимые трансформеры.

Самый часто используемый - это ManifestResourceTransformer. Он необходим для того, чтобы в манифесте runnable jar файла был указан класс со статическим методом main. Как вы знаете, именно он запускается при старте приложения. В элементе mainClass следует указать полное имя класса с main-методом. В приведённом выше примере указан класс ru.blogspot.developer.remarks.MainClass - просто замените его на свой.

Поскольку runnable jar включает в себя все необходимые для запуска библиотеки, может случиться так, что в нескольких из них будут конфигурационные файлы с одинаковым именем. Если не предпринять специальных действий, в целевом jar-файле у нас окажется только часть необходимого контента. К счастью, есть специальный трансформер AppendingTransformer. Если в библиотеках встречаются несколько файлов с одинаковым именем, в runnable jar файле они будут слиты в один файл с тем же именем.

Важно, если вы используете Spring! Там есть библиотеки, которые содержат файлы с одинаковыми именами spring.handlers и spring.schemas. Чтобы обработать их правильно, добавьте в секцию transformers ещё два элемента:

<transformer
        implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
    <resource>META-INF/spring.handlers</resource>
</transformer>
<transformer
        implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
    <resource>META-INF/spring.schemas</resource>
</transformer>

На самом деле трансформеров гораздо больше. Полный их перечень вы можете найти на официальной странице. Там есть специальные обработчики для таких файлов, как licence, notice, components.xml, plugin.xml и прочие.

Теперь соберём наше приложения. Выполните цель mvn:install. В папке target вы получите два архива. Понять, какой из этих файлов можно запускать автономно, можно прежде всего по размеру (автономный значительно больше), а также по наличию в файле META-INF/MANIFEST.MF внутри архива строки "Main-Class: ru.blogspot.developer.remarks.SshManager".

Чтобы не лезть внутрь архивов, вы можете указать, каким суффиксом снабжать runnable jar. В pom.xml за это отвечает секция configuration/shadedClassifierName. В нашем примере указан суффикс executable. Следовательно, файл с executable на конце является автономным.