본문 바로가기
프로그래밍/Web

FastAPI와 Ajax로 SSH 서버 상태 체크하는 웹 서버 만들기

by Hwan,. 2023. 2. 19.
728x90
반응형

개요 및 구성

 웹 페이지에서 여러 대의 SSH 서버가 연결이 가능한지 여부를  UI로 확인하고 싶어서 만들었다.

구성은 Front, Back, SSH Server로 되어 있고 Front, Back은 Ajax를 사용하여 Json으로 데이터를 주고 받는다.

Front는 HTML과 Javascript, jquery-3.6.3.js의 ajax, Back은 FastAPI로 작성해 두었고 서버의 SSH 연결 상태를 확인하기 위해서 paramiko 패키지를 사용한다.

 

아래는 서버의 1대의 상태를 체크하는 과정을 요약한 그림이다.

 

 


Front

 UI는 Server Count에 서버의 대수를 입력하면 아래처럼 해당 수에 맞춰 인터페이스가 추가된다.

localStorage를 사용했기 때문에 페이지가 새로 고침되어도 해당 인터페이스가 유지된다.

3을 입력한 경우
10을 입력한 경우

 

 서버 정보를 입력하고 check를 누르면 위 구성에 따라 요청을 보내고, 결과는 아래와 같이 분류되어 Connection Status에 작성된다.

  • 진행 중인 경우, Work in progress.
  • 제한 시간 내에 연결이 안된 경우, timeout
  • 연결이 성공한 경우, ok
  • Callback 순서(error > complete > fail > always)에 따라 fail되었는데 응답을 못받은 경우, Fail

예시

ssh_server_status_checker.html 파일 

<!DOCTYPE html>
<html lang="ko">
<head>
    <script src="static/js/jquery-3.6.3.js"></script>
    <link rel="stylesheet" href="test.css">
    <meta charset="UTF-8">
    <title>SSH Server Status Checker</title>

    <script>
        var timerId = null;
        var max_serverCount = localStorage.getItem('max_serverCount');
        const server_url = "http://127.0.0.1:8000";

        function set_ServerCount(){
            max_serverCount = $('#text_server_count').val();
            localStorage.setItem('max_serverCount', max_serverCount);
            window.location.reload();
        }

        function statusTimer() {
            for(var i = 1; i <= max_serverCount; i += 1){
                sshServer_statusCheck(i);
            }
        }
        
        function StartClock(interval=3000) {
            timerId = setInterval(statusTimer, interval);
            statusTimer();
        }
        
        function StopClock() {
            if(timerId != null) {
                clearInterval(timerId);
            }
        }

        function sshServer_statusCheck(target){
            var html_result_id = target + "_status_result";
            var server_ip = $('#text_' + target + '_ip').val();
            var server_port = $('#text_' + target + '_port').val();
            var server_id = $('#text_' + target + '_id').val();
            var server_pw = $('#text_' + target + '_pw').val();
            var send_data = {
                key: target,
                ip: server_ip,
                port: server_port,
                id: server_id,
                pw: server_pw,
            };

            $('#' + html_result_id).html("Work in progress.", status);
            document.getElementById(html_result_id).style.color = "#FFEE11";

            $.ajaxSetup({'cache':true});
            $.ajax({
                url : server_url + "/statusCheck",
                type : "POST",
                dataType : "json",
                timeout : 2000,
                contentType : "application/json",
                data : JSON.stringify(send_data),
                success: function(data){
                    $('#' + html_result_id).html(data.status, status);
                    if(data.status == "ok"){
                        document.getElementById(html_result_id).style.color = "green";
                    }
                    else{
                        document.getElementById(html_result_id).style.color = "red";
                    }
                },
                error: function(request, status, error){
                    if(error == "timeout"){
                        $('#' + html_result_id).html(status, 200);
                        document.getElementById(html_result_id).style.color = "red";
                    }
                }
            })
            .fail(function(xhr, textStatus, errorThrown) {
                if ($('#' + html_result_id).text() == "Work in progress."){
                    $('#' + html_result_id).html("Fail", status);
                    document.getElementById(html_result_id).style.color = "red";
                }
            });
        }
    </script>
</head>
<body>
    <h3>SSH Server Status Checker</h3>
    <form class="my-box">
        <div>
            <label><b>Server Count : </b></label> <input type="text" placeholder="number" id="text_server_count" style="width: 50px;"> <input type="button" value="set" onclick="set_ServerCount();"><br>
            <hr>
            <label><b>Input Data</b></label><br>
            <script>
                var tmp_value;

                for(var i = 1; i <= max_serverCount; i += 1){
                    tmp_value = "server" + i;
                    document.write('<label>' + tmp_value + ' Settings : </label>');
                    document.write('<input type="text" id="text_' + tmp_value + '_ip" placeholder="IP Address" style="width: 100px;"> ');
                    document.write('<input type="text" id="text_' + tmp_value + '_port" placeholder="Port" style="width: 50px;"> ');
                    document.write('<input type="text" id="text_' + tmp_value + '_id" placeholder="ID" style="width: 50px;"> ');
                    document.write('<input type="password" id="text_' + tmp_value + '_pw" placeholder="PW" style="width: 50px;"> ');
                    document.write('<input type="button" value="check" onclick="sshServer_statusCheck(' + "'" + tmp_value + "'" + ');"> ');
                    document.write('<br>');
                }
            </script>
        </div>
        <hr>
        <label><b>Connection Status</b></label><br>
        <script>
            for(var i = 1; i <= max_serverCount; i += 1){
                tmp_value = "server" + i;
                document.write('<label>' + tmp_value + ' : </label><label id="server' + i + '_status_result"></label><br>');
            }
        </script>
        <label>Repeat check : </label>
        <input type="button" value="Start" onclick="StartClock();">
        <input type="button" value="End" onclick="StopClock();">
    </form>
    <br>
    <br>
</body>
</html>

 

Back

 아래 파이썬 백엔드 서버는 프론트에서 서버로 전달해준 Json을 파싱하여 내부의 서버 정보로 ssh 연결을 시도한다.

MVC 패턴은 적용하지 않았고, DB도 따로 구축하진 않고 dict_server_info라는 딕셔너리 자료구조를 이용했다.

import paramiko

import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
from starlette.middleware.cors import CORSMiddleware

def ssh_command_sender(ip, port, user, pw, cmds):
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy)
    ssh.connect(ip, port, user, pw, timeout=1)

    stdin, stdout, stderr = ssh.exec_command(";".join(cmds))

    lines = stdout.read()
    res = ''.join(str(lines))
    
    return res

def _health_check(ip, port, user, pw):
    try:
        ssh_command_sender(ip, port=port, user=user, pw=pw, cmds=[])
        return True
    except:
        return False


origins = ["*"]

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

class serverSettings(BaseModel):
    key: str
    ip: str
    port: str
    id: str
    pw: str
    
dict_server_info = {
    #"server1":{"ip":"", "port":"22", "id":"", "pw":"", "status":""},
}

@app.post("/statusCheck")
async def statusCheck(info: serverSettings):
    key = info.key
    try:
        dict_server_info[key]["ip"] = info.ip
        dict_server_info[key]["port"] = info.port
        dict_server_info[key]["id"] = info.id
        dict_server_info[key]["pw"] = info.pw
    except:
        dict_server_info[key] = {"ip":info.ip, "port":info.port, "id":info.id, "pw":info.pw, "status":""}
        
    print(dict_server_info[key])

    print(f"{key} health check ...", end="")
    if _health_check(dict_server_info[key]["ip"], dict_server_info[key]["port"] , dict_server_info[key]["id"], dict_server_info[key]["pw"]) == False:
        print("error")
        return {"status":f"fail - {key} connect"}
    
    print("ok")
    dict_server_info[key]["status"] = "ok"
    return {"status":"ok"}
 

if __name__ == "__main__":
    uvicorn.run(app, port=8000, reload=False)

 

아래는 실제로 SSH 서버 vm을 실행하고 테스트해본 이미지이다.

1개의 서버를 체크할 경우 서버의 상태가 정상적으로 확인되었다.

1개 서버 체크
백엔드 로그 확인

 

 여러 개 서버의 경우로 우측 check 버튼을 통해 체크할 경우 다양한 현재 상태가 출력된다.

여러 개 서버를 각자 체크하기


주기적인 상태 체크

 위에서 체크 기능이 잘 동작하는걸 봤지만 프로그램이 내가 원하는 역할을 수행하게 하려면 입력한 정보를 일정 주기로 반복하여 체크할 수 있어야 한다. Repeat Check는 Timer를 활용하여 일정 주기(interval)로 ssh 연결을 시도한다.

 

 


후기

 간단한 기능을 제공하는 서버긴하지만 확장할 수 있는 내용들이 많다.

프론트에서는 html, css, js 파일을 분리하여 관리/ 확장성을 높이거나 React.js를 활용하여 더 깔끔하고 모던한 웹 사이트로 개선할 수 있고, 백엔드에서도 보안(openssl, oauth)과 디자인 패턴(MVC)을 적용하고 NoSQL DB와 연동해 볼 수 있을 듯 하다. 또한 DevOps 관점에서는 컨테이너화 시켜 쿠버네티스에 배포하고 GitOps를 적용해볼 수도 있다.

퇴근 후와 주말에만 가끔식 하는 프로젝트라 진행은 더디지만 하나씩 공부해서 Pluto에도 적용해보겠다.

728x90
반응형

'프로그래밍 > Web' 카테고리의 다른 글

[백엔드] Flask에서 JWT 토큰 생성하기  (0) 2022.09.18
백엔드 로드맵 정리  (0) 2022.09.12
[Django] 장고 기본 웹서버 띄우기  (0) 2022.03.28

댓글