Вступ
Уявіть, що ви тримаєте одного пінгвіна як домашнього улюбленця — назвемо його Лінукс. Як опікун, вам потрібно годувати його, прибирати в його просторі і відводити до ветеринара, якщо це необхідно.
Управляти одним пінгвіном може бути не настільки складно — нехай і практично це не завжди так. Але що, якщо у вас 20 пінгвінів? Або 100?
Щоб ускладнити ситуацію, пінгвіни мають підкатегорії, кожна з яких потребує різних умов для життя і різних переваг у їжі.
Що б ви робили, якби вас поставили перед завданням регулярно та стабільно керувати всіма ними?
Мета
З таким дещо смішним вступом, ось реальна проблема: більше 100 серверів Linux потребують щомісячних оновлень.
Замість того, щоб вручну вводити команди для кожної дистрибуції, я вирішив автоматизувати цей процес.
Підхід
Щоб зменшити навантаження на оператора, рішення працює лише з jar, CSV і однією командою.
Імена користувачів і приватні ключі для SSH-з’єднань зберігаються однаковими.
Шляхи до необхідних файлів для наступних завдань залишаються фіксованими:
1. Приватний ключ
2. Публічний ключ
3. CSV файл
4. Каталог для логів
Кроки
- Прочитати CSV файл
- Встановити SSH-з’єднання за допомогою автентифікації за публічним ключем
- Виконати команди, що відповідають кожній дистрибуції
- Записати результати у логи
Середовище
Цільові сервери — це сервери Linux, які працюють на EC2 інстансах AWS.
Щоб зменшити час розробки, я використовував Java 1.8 і Spring Boot, оскільки вони вже були встановлені на моїй робочій станції.
Однак, оскільки це Java 1.8, перенесення рішення на Python або C# не буде складним.
Посилання
https://github.com/hiromaki58/white-box/tree/main/linux_updating
Читання CSV файлу
Список серверів, що потребують оновлень, створюється у форматі CSV.
Причина вибору CSV полягає в тому, що його легко створити в Excel, і будь-які зміни щодо цільових серверів можна вносити без зайвих зусиль.
Ось приклад CSV.
CompanyA,AHostName,xxx.xxx.xxx.xxx,Ubuntu
CompanyB,BHostName,xxx.xxx.xxx.xxx,RedHat
Читання CSV
private List getColumnInfoList(String filePath, int columnIndex, String columnTitle){
final int csvColumnCount = 4;
List inputList = new ArrayList<>();
try(BufferedReader br = new BufferedReader(new FileReader(filePath))){
String line;
while((line = br.readLine()) != null){
String[] columnList = line.split(",");
if(columnList.length > csvColumnCount - 1 && columnList[columnIndex].trim().isEmpty()){
System.err.println("Warning : Missing the value in column " + columnTitle);
}
if(columnList.length > csvColumnCount - 1){
inputList.add(columnList[columnIndex]);
}
}
}
catch(IOException e){
System.err.println("Fail to read the file for " + columnTitle);
}
return inputList;
}
Розподіл CSV файлу для наступного процесу
public List getHostNameList(String filePath) {
final int hostNamePositionNum = 1;
final String columnTitle = "Hostname";
return getColumnInfoList(filePath, hostNamePositionNum, columnTitle);
}
public List getIpAddrList(String filePath) {
final int ipAddrPositionNum = 2;
final String columnTitle = "IpAddress";
return getColumnInfoList(filePath, ipAddrPositionNum, columnTitle);
}
public List getDistributionList(String filePath){
final int distributionPositionNum = 3;
final String columnTitle = "Distribution";
return getColumnInfoList(filePath, distributionPositionNum, columnTitle);
}
Доступ через SSH за допомогою криптографії з публічним ключем
public void connect(List hostNameList, List ipAddrList, List distributionList) {
SshClient client = SshClient.setUpDefaultClient();
client.start();
for(int i = 0; i < hostNameList.size(); i++){
String userName= userNameProviderFactory.getUserName(distributionList.get(i));
try (ClientSession session = client.connect(userName, ipAddrList.get(i), port).verify(10000).getSession()) {
char[] passphrase = pass.toCharArray();
KeyPair keyPair = SshKeyCreater.loadKeyPair(privateKeyPath, publicKeyPath, passphrase);
session.addPublicKeyIdentity(keyPair);
session.auth().verify(5000);
sendCommandList(session, hostNameList.get(i), distributionList.get(i));
session.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
client.stop();
int exitCode = SpringApplication.exit(context, () -> 0);
System.exit(exitCode);
}
Читання ключа
public static KeyPair loadKeyPair(String privateKeyPath, String publicKeyPath, char[] passphrase) throws Exception {
// Читання секретного ключа
PEMParser pemParser = new PEMParser(new FileReader(privateKeyPath));
Object object = pemParser.readObject();
pemParser.close();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
PrivateKey privateKey;
// Декодування секретного ключа з паролем
if (object instanceof PEMEncryptedKeyPair) {
PEMEncryptedKeyPair encryptedKeyPair = (PEMEncryptedKeyPair) object;
PEMKeyPair pemKeyPair = encryptedKeyPair.decryptKeyPair(new JcePEMDecryptorProviderBuilder().build(passphrase));
privateKey = converter.getPrivateKey(pemKeyPair.getPrivateKeyInfo());
}
// Без пароля
else if (object instanceof PEMKeyPair) {
privateKey = converter.getPrivateKey(((PEMKeyPair) object).getPrivateKeyInfo());
}
else {
throw new IllegalArgumentException("Невірний формат приватного ключа.");
}
// Читання публічного ключа
PublicKey publicKey = loadOpenSSHPublicKey(publicKeyPath);
return new KeyPair(publicKey, privateKey);
}
Виконання команд залежно від дистрибутиву
Наступні кроки для дистрибутиву Ubuntu:
private void sendCommandList(ClientSession session, String hostName, String distribution){
List commnadList;
commnadList = CommandListProviderFactory.getCommandListProvider(distribution).getCommandList();
for(Command commandSet : commnadList){
sendCommand(session, hostName, commandSet);
}
}
public static CommandListProvider getCommandListProvider(String distribution){
if(distribution.equalsIgnoreCase("Ubuntu")){
return new UbuntuCommandListProvider();
}
else if(distribution.equalsIgnoreCase("RedHat")){
return new RedHatCommandListProvider();
}
throw new IllegalArgumentException();
}
public class UbuntuCommandListProvider implements CommandListProvider{
@Override
public List getCommandList(){
return CommandList.getUbuntuCommandList();
}
}
public static List getUbuntuCommandList(){
List commandList = new ArrayList<>();
commandList.add(new Command("hostname", null, true, false));
commandList.add(new Command("cat /etc/issue", null, true, false));
commandList.add(new Command("sudo apt update", null, false, false));
commandList.add(new Command("sudo apt upgrade", null, false, true));
commandList.add(new Command("ps aux | grep apache2 | grep -v grep", null, true, false));
return commandList;
}
Обробка запитів на підтвердження під час виконання команд
При виконанні команд сервер може запитувати підтвердження.
Цей розділ описує, як обробляти ці запити на підтвердження.
private void sendCommand(ClientSession session, String hostName, Command commandSet){
String responseString;
try (ByteArrayOutputStream responseStream = new ByteArrayOutputStream();
ClientChannel channel = session.createExecChannel(commandSet.getCommand())) {
channel.setOut(responseStream);
channel.open().verify(5, TimeUnit.SECONDS);
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), TimeUnit.SECONDS.toMillis(60));
responseString = new String(responseStream.toByteArray());
// У випадку, якщо команда не дала жодної відповіді
if((responseString.contains("command not found") || responseString.contains("コマンドがありません")) && commandSet.getAlternativeCommand() != null){
sendCommand(session, hostName, commandSet);
return;
}
// Показати відповідь в терміналі і запитати, чи продовжувати
if(commandSet.getIsContinuedOrNo()){
Scanner scan = new Scanner(System.in);
if(terminalHandler.checkOutputAndWaitForEnterKey(commandSet, responseString, scan)){
return;
};
}
if(commandSet.isAskedToSayYesOrNo()){
Scanner scan = new Scanner(System.in);
String userInput = terminalHandler.inputYesOrNo(commandSet, responseString, scan);
OutputStream out = channel.getInvertedIn();
out.write((userInput + "\\n").getBytes());
out.flush();
}
logCreater.saveLog(hostName, commandSet.getCommand());
logCreater.saveLog(hostName, " ");
logCreater.saveLog(hostName, responseString);
if(!channel.isClosed()){
channel.close();
}
}
catch (IOException e){
e.printStackTrace();
}
}
Запис результатів виконання команд у журнали
public void saveLog(String hostName, String logContent){
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
String date = dateFormat.format(new Date());
String logFileTitle = logStoragePath + File.separator + date + "-" + hostName + ".txt";
try(BufferedWriter bw = new BufferedWriter(new FileWriter(logFileTitle, true))){
bw.write(logContent);
}
catch(IOException e){
System.err.println("Не вдалося зберегти журнал для хосту " + hostName);
e.printStackTrace();
}
}
Перекладено з: [Automating commands, instead of manually entering over 100 Linux servers](https://medium.com/@hiromaki58/automating-commands-instead-of-manually-entering-over-100-linux-servers-fb435ac5e868)