fabric 으로 원격 호스트에서 접속하고 로그를 가져올 때 연결되어 있는 호스트가 복잡하고 로그 path 로 여러개가 존재하면
단일 함수로 작성하는 것이 힘들고 함수 자체고 너무 커지고 복잡해져서 디버깅이 힘듭니다.
그리고 로그를 가져온 후에 로그를 원하는 파트로 분해하는 parsing 과정이 필요합니다.

즉, 호스트가 매우 많고 path 도 여러개이고 parsing 도 복잡한 과정이 필요하면 각각에 많는 모듈로 분해를 할 필요가 있습니다.
호스트 몇개 없을 때는 문제가 발생하지 않지만 호스트가 매우 많다면 각각을 담담할 모듈로 분해를 해야합니다.

모듈 설명
SshManager ssh connection 연결을 담당
LogSearchScheduler 로그 검색을 Step 별로 나누어 각 Step 별 검색 스케줄링을 담당
LogParser  로그 파싱을 담당
LogSearchManager 그 검색의 메인 역할을 담당하고 각 모듈을 통합


1) SshManager

Python의 FabricSSH 연결을 기반으로 동작합니다.

Fabric은 원격 서버에 SSH로 접속해서 명령을 실행하거나 파일을 업로드/다운로드하는 작업을 간편하게 자동화할 수 있는 라이브러리입니다. 주로 배포 자동화서버 관리 등의 작업에 많이 사용됩니다.

핵심은 paramiko를 사용해 SSH 연결을 시도하는 것이며, 맞는 비밀번호를 찾으면 해당 정보를 반환합니다.

아래 코드는 git 을 통해 다운받을 수 있습니다.

git clone git@github.com:jbpark/jbDeskExample.git
cd jbDeskExample/jbDesk/ch3.3

 

ssh_util.py

import logging

import paramiko

from lib.util.encoding_util import decrypt_cipher_text

# logging.basicConfig(level=logging.DEBUG)

def get_ssh_user_info(ssh_user_infos, host_ip, first_index, check_sudo=True):
    attempts = 0  # Counter for keeping track of attempts

    sorted_infos = sorted(ssh_user_infos, key=lambda x: (x.index != first_index, x.index))

    for ssh_connect_info in sorted_infos:
        user_name = ssh_connect_info.user_name
        password = decrypt_cipher_text(ssh_connect_info.password)
        print(f"Attempting attempts:{attempts}, index:{ssh_connect_info.index}")

        client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        try:
            client.connect(host_ip, username=user_name, password=password, timeout=2)
            print(f"[>] Valid password found index:{ssh_connect_info.index}")
            client.close()
            return ssh_connect_info
        except paramiko.AuthenticationException:
            print("Invalid password!")
        except Exception as e:
            print(f"Connection failed: {e}")
        finally:
            client.close()

        attempts += 1  # Increment the attempts counter for each password

    return None

 

🔍 주요 기능 요약

  • ssh_user_infos: 여러 개의 SSH 사용자 정보를 가진 객체 리스트 (아마 사용자명, 암호화된 비밀번호, 우선순위 index 포함).
  • host_ip: 접속하려는 원격 서버의 IP 주소.
  • first_index: 우선적으로 시도할 계정 index.
  • decrypt_cipher_text: 암호화된 비밀번호를 복호화하는 함수.

✅ 동작 흐름

  1. first_index 기준으로 우선순위를 두어 정렬 (first_index를 먼저 시도).
  2. 각 사용자 정보로 SSH 접속 시도.
  3. 접속 성공하면 해당 사용자 정보를 반환.
  4. 실패하면 다음 사용자 정보로 계속 시도.
  5. 모두 실패하면 None 반환.

 

SSH 를 시도하는 계정 정보는 settings 폴더 하위에 xxx.yaml 파일로 저장하면 됩니다.

SshManager 는 fabric 으로 로그를 가져 오기 전에 해당 호스트에 접속 가능한 계정 정보를 찾습니다.

이 때 yaml 파일에 등록된 계정 정보를 읽어서 사용합니다.

ssh.yaml

SSH:
  - user_name: root
    password: gAAAAABn9K2pfYmSQT0c4Ay4qenyD99ffiWxTQPDOtIa98j9H8WTZ0HpaXfXEvETFLP-Tz59UJVMMBIFw5LIjxBSjlCD4wSOLw==

  - user_name: vagrant
    password: gAAAAABn9K2pfYmSQT0c4Ay4qenyD99ffiWxTQPDOtIa98j9H8WTZ0HpaXfXEvETFLP-Tz59UJVMMBIFw5LIjxBSjlCD4wSOLw==

 

위 YAML 파일은 SSH 접속에 사용할 계정 정보 리스트를 담고 있습니다.

🧾 각 항목 설명:

필드 이름 설명
SSH 최상위 키로, SSH 관련 정보를 담는 리스트
user_name SSH 접속에 사용할 사용자명 (예: root, vagrant)
password 암호화된 비밀번호 문자열 (Fernet 로 암호화)

 

encoding_util.py

from cryptography.fernet import Fernet

from lib.models.constants.config_key import LDAP_KEY


def decrypt_cipher_text(ciphered_text):
    cipher_suite = Fernet(LDAP_KEY)
    try:
        result = str((cipher_suite.decrypt(ciphered_text.encode())), 'utf-8')
    except Exception as e:
        result = None
        print(f"An error occurred: {e}")

    return result


def encrypt_cipher_text(text):
    cipher_suite = Fernet(LDAP_KEY)
    try:
        result = str(cipher_suite.encrypt(text.encode()).decode())
    except Exception as e:
        result = None
        print(f"An error occurred: {e}")

    return result

이 코드는 cryptography 라이브러리의 Fernet 모듈을 이용하여 문자열을 암호화/복호화하는 유틸리티 함수들을 정의하고 있습니다. 주로 비밀번호, API 키, 인증 정보 같은 민감한 데이터를 안전하게 처리하기 위해 사용됩니다.


🔐 핵심 개념 요약

  • Fernet은 대칭키(하나의 키) 기반 암호화 방식입니다.
  • LDAP_KEY는 암호화와 복호화에 사용되는 비밀 키입니다.
  • 암호화된 문자열은 일반적으로 gAAAAA... 형태로 시작합니다.

📌 요약

함수 이름 설명
encrypt_cipher_text 평문을 Fernet으로 암호화
decrypt_cipher_text 암호화된 문자열을 복호화
LDAP_KEY 암호화/복호화에 사용되는 비밀 키

 

2) LogSearchScheduler

주어진 조건에 따라 로그 검색 단계를 스케줄링하고 연결 정보를 준비하는 로직을 담고 있습니다. 이 클래스는 BaseLogSearchScheduler라는 부모 클래스를 상속받아 동작합니다.


📌 정리


항목 설명
LogSearchScheduler 로그 검색 단계별 작업을 스케줄링하는 클래스
get_step_connect_infos 주어진 단계에 맞는 SSH 등 연결 정보 설정
schedule_steps 서비스 유형에 따라 로그 검색 단계를 설정
사용 예 멀티 스텝 로그 분석 (예: Gateway → API → Echo 순으로 연결 확인 및 로그 추출)

 

다음은 로그 검색 스케줄링 시스템의 베이스 클래스, BaseLogSearchScheduler의 정의입니다.

base_log_search_scheduler.py

from lib.models.constants.const_response import RespStatus, RespMessage
from lib.models.log.respone.log_search_response import LogSearchResponse
from lib.util.config_util import load_service_connect_infos_from_yaml, load_ssh_user_infos_from_yaml


class BaseLogSearchScheduler:
    def __init__(self, manager, yaml_loader, config_loader):
        self.manager = manager
        self.yaml_loader = yaml_loader
        self.config_loader = config_loader
        self.env = manager.env
        self.keyword = manager.keyword
        self.service_name = manager.service_name
        self.level = manager.level
        self.logs = None
        self.index = 0
        self.total = 0

        # 전체 main steps
        self.all_main_steps = []

        # 현재 main step
        self.current_main_step = None

        # 전체 sub steps
        self.all_sub_steps = None

        # 현재 sub step
        self.current_sub_step = None

        self.step_connect_infos = None

        self.all_connect_infos = self.get_all_connect_infos()
        self.env_connect_infos = self.get_env_connect_infos()
        self.ssh_connect_infos = self.get_ssh_connect_infos()

    def get_all_connect_infos(self):
        if self.env is None:
            return None

        return load_service_connect_infos_from_yaml(self.yaml_loader)

    def get_env_connect_infos(self):
        env_connect_infos = []

        for item in self.all_connect_infos:
            if item.env.upper() != self.env.upper():
                continue

            env_connect_infos.append(item)

        return env_connect_infos

    def exist_main_step(self):
        if self.all_main_steps:
            return True
        else:
            return False

    def exist_sub_step(self):
        if self.all_sub_steps:
            return True
        else:
            return False

    # log search step 을 리턴함
    def get_current_main_step(self):
        return self.current_main_step
    
    # sub steps 를 스케줄링
    def schedule_sub_steps(self):
        print("schedule_sub_steps")

    def get_next_main_step(self):
        if not self.all_main_steps:
            return None

        self.current_main_step = self.all_main_steps.pop(0)
        return self.current_main_step

    def get_all_sub_steps(self):
        return self.all_sub_steps

    def get_next_sub_step(self):
        if not self.all_sub_steps:
            return None

        self.current_sub_step = self.all_sub_steps.pop(0)
        return self.current_sub_step

    # log search step 을 리턴함
    def get_step_connect_infos(self, step):
        return self.step_connect_infos

    def ensure_step_connect_info(self):
        print("")

    def get_step_log_path(self, step):
        return self.step_log_path

    def get_ssh_connect_infos(self):
        return load_ssh_user_infos_from_yaml(self.yaml_loader)

    def setLogs(self, logs):
        self.logs = logs

    def get_failed_response(self, message):
        response = LogSearchResponse()
        response.command_type = "log"
        response.status = RespStatus.FAILED.value
        response.message = message
        response.index = self.index
        response.total = self.total
        return response

    def get_success_response(self):
        response = LogSearchResponse()
        response.command_type = "log"
        response.status = RespStatus.SUCCESS.value
        response.message = RespMessage.SUCCESS.value
        response.index = self.index
        response.total = self.total
        return response

    def get_connect_infos_by_service_name(self, service_name):
        connect_infos = []

        for item in self.env_connect_infos:
            if item.service.service_name != service_name:
                continue

            connect_infos.append(item)

        return connect_infos

 

여러 서버 환경 및 서비스 구성 정보를 바탕으로 로그 검색 단계를 스케줄링하고, 연결 정보를 관리하는 핵심 역할을 합니다.


📦 주요 기능 정리

  설명
환경 설정 로딩 YAML에서 서비스 및 SSH 연결 정보 로드
단계 관리 로그 검색 단계(main/sub step) 관리
연결 정보 제공 특정 서비스/환경에 맞는 SSH 및 서비스 연결 정보 반환
응답 생성 성공/실패 응답(LogSearchResponse) 생성

 

3) LogParser 

로그 파싱은 정규식을 사용합니다.

이 코드는 로그 분석이나 정규식 기반의 파싱 시스템에서 정규표현식 패턴을 정의하는 상수 집합입니다. RegPattern 클래스는 다양한 로그 항목을 추출하기 위해 사용되는 파라미터화된 정규표현식 템플릿을 제공합니다. 즉, 특정 필드명을 지정하면 그것에 맞는 정규식을 생성할 수 있도록 설계되어 있습니다.

reg_pattern.py

from lib.models.log.log_pattern import LogPattern


class RegPattern:
    WORD = r'(?P<%s>\S+)'
    B_WORD = r'\[(?P<%s>\S+)\]'
    B_WORD_AST = r'\[(?P<%s>\S*)\]'
    TIME_RECEIVED = r'\[(?P<%s>[\w:/]+\s[+\-]\d{4})\]'
    TIMESTAMP = r'(?P<%s>\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2},\d{1,3})'
    REQUEST_FIRST_LINE = r'"(?P<%s>(\S+) (\S+)\s*(\S+)\s*)"'
    STATUS = r'(?P<%s>\d{3})'
    USER_AGENT = r'"(?P<%s>(\S+)\s*(\S+))"'
    DQ_WORD = r'"(?P<%s>\S+)"'
    DIGIT_MS = r'(?P<%s>\d{1,}ms)'
    LEVEL = r'(?P<%s>(INFO|ERROR|WARN|TRACE|DEBUG|FATAL))'
    B_TRACE_SPAN_PARENT_ID = r'\[(?P<%s>[A-Za-z0-9]*,[A-Za-z0-9]*,[A-Za-z0-9]*)\]'
    ANY = r'(?P<%s>.*)'
    RESOURCE = r'resource=(?P<%s>\d{1,5}),'
    STATUS_CODE = r'"status_code":"(?P<%s>[A-Za-z0-9]*)"'
    REQUEST_ID = r'"request_id":"(?P<%s>[A-Za-z0-9]*)"'

    SPACE = LogPattern(r'\s', None)
    SPACE_MORE = LogPattern(r'\s+', None)
    COLON = LogPattern(r':', None)
    HYPHEN = LogPattern(r'-', None)

🔍 구조 설명

📦 클래스: RegPattern

이 클래스는 모두 클래스 변수(class-level constant)로 정의되어 있으며, 로그 문자열을 파싱하기 위해 활용됩니다.


📌 주요 패턴 설명

변수명설명정규표현식비고

 

변수명 설명 정규표현식 비고
WORD 공백 없는 단어 (?P<%s>\S+) 그룹 이름 지정 가능
B_WORD 대괄호로 둘러싸인 단어 \[(?P<%s>\S+)\] 예: [INFO]
B_WORD_AST 빈 문자열도 허용 \[(?P<%s>\S*)\] 빈 문자열도 가능
TIME_RECEIVED Apache 로그 시간 \[(?P<%s>[\w:/]+\s[+\-]\d{4})\] 예: [10/Oct/2000:13:55:36 -0700]
TIMESTAMP 일반적인 타임스탬프 (?P<%s>\d{4}-\d{2}-\d{2} ... ) 예: 2023-09-01 12:30:45,123
REQUEST_FIRST_LINE HTTP 요청 첫 줄 "(?P<%s>(\S+) (\S+)\s*(\S+)\s*)" 예: "GET /index.html HTTP/1.1"
STATUS HTTP 상태 코드 (?P<%s>\d{3}) 예: 200, 404
USER_AGENT User-Agent 파싱 "(?P<%s>(\S+)\s*(\S+))"  
DQ_WORD 큰따옴표로 둘러싸인 단어 "(?P<%s>\S+)"  
DIGIT_MS ms 단위 시간 (?P<%s>\d{1,}ms) 예: 300ms
LEVEL 로그 레벨 `(?P<%s>(INFO ERROR
B_TRACE_SPAN_PARENT_ID Trace 정보 \[(?P<%s>...) 예: [abc123,def456,ghi789]
ANY 아무 문자열 (?P<%s>.*)  
RESOURCE resource 정보 resource=(?P<%s>\d{1,5}),  
STATUS_CODE JSON 로그의 status_code "status_code":"(?P<%s>[A-Za-z0-9]*)"  
REQUEST_ID JSON 로그의 request_id "request_id":"(?P<%s>[A-Za-z0-9]*)"  

 

4) 코드 실행

Vagrant 파일은 3.2편에서 만든 vagrant 파일을 사용했습니다.

아래와 같이 gateway01, api01, echo01 VM 은 실행 중이어야 합니다.

 

jbdesk.py 를 실행합니다.

위 서버의 로그를 검색하기 위해서는 Tid 를 입력하고 Search 를 클릭하면 됩니다.

Posted by 제이브레인
,

pyhton fabric 을 사용하여 원격 서버의 로그를 추적할 수 있습니다.

예를 들면 api 요청이 들어올 때 다음과 같이 여러 단계를 거치면서 처리가  gateway → api → db 됩니다.

TID(Trace ID) 가 존재하는 경우 각각 서비스에서 TID 로 검색하면 됩니다.

그런데 gateway, api, db 에서 로그를 검색하게 되면 3군데를 모두 검색해야 합니다.

그런데 서비스가 좀더 복잡하여 여러군데를 거치면 추적은 더욱 힘들어집니다.

fabric 을 사용하면 이러한 것을 자동으로 할 수  있습니다.

1) 테스트 환경

우선 코드를 만들 때 여러 단계를 거치는 테스트 환경을 만들어야 합니다.

vagrant 를 사용하야 해당 환경을 만들어보겠습니다.

ChatGPT  에 사용한 프롬프트입니다.

vagrant 로 다음과 같이 구축해주세요.
api 요청이 gateway01 로 오면 api01 의 api 를 호출하고 api01 은 echo01 를 호출합니다.
echo01 은 들어온 요청에 대해 echo message 를 리턴합니다.
python flask 로 구축해주세요.
TID(Trace ID) 도 전달하여 api 호출이 왔을 때 file 로 log 메시지도 출력해주세요.

1. host_name : gateway01 
ip : 192.168.56.40
os : rocky9

2. host_name : api01
ip : 192.168.56.40
os : rocky9

3. host_name : echo01
ip : 192.168.56.40
os : rocky9

 

Flask 기반의 gateway → api → echo 구조Vagrant + Rocky 9으로 구성하고, **TID(Trace ID)**를 HTTP 헤더로 전달하여 파일로 로그를 저장하는 구조로 만들어보겠습니다.


✅ 시스템 요약

구성 요소 역할 IP 포트
gateway01 외부 요청 수신 및 전달 192.168.56.40 5000
api01 비즈니스 로직 처리 및 전달 192.168.56.41 5001
echo01 메시지 반환 및 로그 기록 192.168.56.42 5002

1️⃣ Vagrantfile 설정

Vagrant.configure("2") do |config|
  config.vm.box = "generic/rocky9"

  def setup_vm(vm, hostname, ip, script_name, py_content)
    vm.vm.hostname = hostname
    vm.vm.network "private_network", ip: ip
    vm.vm.provider "virtualbox" do |vb|
      vb.memory = 512
    end

    vm.vm.provision "shell", inline: <<-SHELL
      echo "📦 Setting up #{hostname}..."

      # Install Python and Flask
      sudo dnf install -y python3 python3-pip
      pip3 install flask requests

      # Create Python script
      cat <<EOF > /home/vagrant/#{script_name}
#{py_content}
EOF

      # Make it executable and run in background
      nohup python3 /home/vagrant/#{script_name} > /dev/null 2>&1 &
    SHELL
  end

  # gateway01
  config.vm.define "gateway01" do |gateway|
    setup_vm(gateway, "gateway01", "192.168.56.40", "gateway_server.py", <<-PYTHON
from flask import Flask, request
import requests, uuid, logging

app = Flask(__name__)
logging.basicConfig(filename="/home/vagrant/gateway.log", level=logging.INFO,
                    format="%(asctime)s [%(levelname)s] TID=%(tid)s: %(message)s")

@app.route("/gateway", methods=["GET"])
def gateway():
    tid = str(uuid.uuid4())
    app.logger.info("Incoming request", extra={"tid": tid})
    res = requests.get("http://192.168.56.41:5001/api", headers={"TID": tid})
    return res.json()

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
    PYTHON
    )
  end

  # api01
  config.vm.define "api01" do |api|
    setup_vm(api, "api01", "192.168.56.41", "api_server.py", <<-PYTHON
from flask import Flask, request
import requests, logging

app = Flask(__name__)
logging.basicConfig(filename="/home/vagrant/api.log", level=logging.INFO,
                    format="%(asctime)s [%(levelname)s] TID=%(tid)s: %(message)s")

@app.route("/api", methods=["GET"])
def api():
    tid = request.headers.get("TID", "no-tid")
    app.logger.info("Forwarding to echo01", extra={"tid": tid})
    res = requests.get("http://192.168.56.42:5002/echo", headers={"TID": tid})
    return res.json()

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5001)
    PYTHON
    )
  end

  # echo01
  config.vm.define "echo01" do |echo|
    setup_vm(echo, "echo01", "192.168.56.42", "echo_server.py", <<-PYTHON
from flask import Flask, request
import logging

app = Flask(__name__)
logging.basicConfig(filename="/home/vagrant/echo.log", level=logging.INFO,
                    format="%(asctime)s [%(levelname)s] TID=%(tid)s: %(message)s")

@app.route("/echo", methods=["GET"])
def echo():
    tid = request.headers.get("TID", "no-tid")
    app.logger.info("Echo received", extra={"tid": tid})
    return {"message": "Echo from echo01", "TID": tid}

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5002)
    PYTHON
    )
  end
end

 


🧪 사용 방법

vagrant up
 

→ 모든 VM이 부팅되며 Flask 서버가 자동 실행됩니다.


🧪 테스트

 
curl http://192.168.56.40:5000/gateway

 

실행 시 예시입니다.

D:\repo\jbDeskExample\jbdesk\ch3.2\vagrant_msa_vm>curl http://192.168.56.40:5000/gateway
{"TID":"85bb1038-581c-43b1-85c0-98bec6f6e613","message":"Echo from echo01"}

 

로그 확인 (각 VM에서):

cat /home/vagrant/gateway.log
cat /home/vagrant/api.log
cat /home/vagrant/echo.log
 

2) 로그 추적 코드 생성 (커맨드)

ChatGPT 에 사용한 프롬프트입니다.

python fabric 으로 TID 로 로그 메시지를 추적하는 코드를 만들어주세요.

1. host_name : gateway01 
ip : 192.168.56.40
os : rocky9
log : /home/vagrant/gateway.log

2. host_name : api01
ip : 192.168.56.40
os : rocky9
log : /home/vagrant/api.log

3. host_name : echo01
ip : 192.168.56.40
os : rocky9
log : /home/vagrant/echo.log

 

VM에 SSH 접속할 때 password가 vagrant로 동일하다는 가정하에, Fabric의 connect_kwargs를 사용해 비밀번호 기반 SSH 인증을 추가해드릴게요.


✅ log_search.py (비밀번호 자동 입력 포함)

from fabric import Connection
import sys

# VM 정보 (각 호스트 이름과 IP)
hosts = {
    "gateway01": "192.168.56.40",
    "api01": "192.168.56.41",
    "echo01": "192.168.56.42"
}

# VM 로그 파일 경로
log_paths = {
    "gateway01": "/home/vagrant/gateway.log",
    "api01": "/home/vagrant/api.log",
    "echo01": "/home/vagrant/echo.log"
}

# 공통 사용자 및 비밀번호
user = "vagrant"
password = "vagrant"

def search_tid(tid):
    for name, ip in hosts.items():
        log_file = log_paths[name]
        print(f"\n🔍 Searching TID={tid} in {name} ({ip})...")
        try:
            conn = Connection(
                host=ip,
                user=user,
                connect_kwargs={"password": password}
            )
            result = conn.run(f"grep {tid} {log_file}", hide=True, warn=True)
            if result.stdout.strip():
                print(result.stdout.strip())
            else:
                print("❌ No log found.")
        except Exception as e:
            print(f"⚠️ Error connecting to {name}: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("❗ Usage: python log_search.py <TID>")
        sys.exit(1)

    tid = sys.argv[1]
    search_tid(tid)

✅ 사용 방법

 
python log_search.py <TID>

예:

(jbdesk38_64) PS D:\repo\jbDeskExample\jbdesk\ch3.2> python log_search.py 85bb1038-581c-43b1-85c0-98bec6f6e613

🔍 Searching TID=85bb1038-581c-43b1-85c0-98bec6f6e613 in gateway01 (192.168.56.40)...
2025-04-08 11:02:21,136 [INFO] TID=85bb1038-581c-43b1-85c0-98bec6f6e613: Incoming request

🔍 Searching TID=85bb1038-581c-43b1-85c0-98bec6f6e613 in api01 (192.168.56.41)...
2025-04-08 11:02:21,143 [INFO] TID=85bb1038-581c-43b1-85c0-98bec6f6e613: Forwarding to echo01

🔍 Searching TID=85bb1038-581c-43b1-85c0-98bec6f6e613 in echo01 (192.168.56.42)...
2025-04-08 11:02:21,150 [INFO] TID=85bb1038-581c-43b1-85c0-98bec6f6e613: Echo received

 

3) 로그 추적 코드 생성 (GUI)

PyQt5 로 GUI 버전을 만들어보겠습니다.

ChatGPT 프롬프트입니다.

PyQt5 로 TID 입력 후 Search 버튼을 클릭하면
찾은 로그 메시지를 Grid 에 출력하는 예제를 만들어주세요.
user_name 은 vagrant, password 는 vagrant 를 입력해주세요.

1. host_name : gateway01 
ip : 192.168.56.40
log : /home/vagrant/gateway.log

2. host_name : api01
ip : 192.168.56.41
log : /home/vagrant/api.log

3. host_name : echo01
ip : 192.168.56.42
log : /home/vagrant/echo.log

 

다음은 PyQt5를 사용하여 TID를 입력하고 "Search" 버튼을 누르면, 정의된 3개의 로그 파일에서 해당 TID가 포함된 로그 메시지를 검색하여 Grid(QTableWidget) 에 출력하는 완전한 예제입니다.


✅ 요구사항 반영 내용

  • TID 입력
  • "Search" 버튼 클릭 시 원격 서버(192.168.56.40, 192.168.56.41, 192.168.56.42) 접속
  • 각각의 로그 파일 검색:
    • /home/vagrant/gateway.log
    • /home/vagrant/api.log
    • /home/vagrant/echo.log
  • 결과를 QTableWidget 에 표시
  • SSH 사용자 정보: user_name = vagrant, password = vagrant

✅ 코드: log_search_gui.py

import sys
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout,
    QLabel, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem
)
from PyQt5.QtCore import Qt
from fabric import Connection

# 서버 및 로그 정보
LOG_SOURCES = [
    {"host_name": "gateway01", "ip": "192.168.56.40", "log_path": "/home/vagrant/gateway.log"},
    {"host_name": "api01",     "ip": "192.168.56.41", "log_path": "/home/vagrant/api.log"},
    {"host_name": "echo01",    "ip": "192.168.56.42", "log_path": "/home/vagrant/echo.log"},
]

# 고정된 SSH 로그인 정보
SSH_USER = "vagrant"
SSH_PASSWORD = "vagrant"

class LogSearchApp(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("TID 로그 검색기")
        self.setGeometry(100, 100, 1000, 500)
        self.init_ui()

    def init_ui(self):
        layout = QVBoxLayout()

        # TID 입력 및 버튼
        input_layout = QHBoxLayout()
        self.tid_input = QLineEdit()
        self.tid_input.setPlaceholderText("검색할 TID 입력")
        self.search_button = QPushButton("Search")
        self.search_button.clicked.connect(self.search_logs)

        input_layout.addWidget(QLabel("TID:"))
        input_layout.addWidget(self.tid_input)
        input_layout.addWidget(self.search_button)
        layout.addLayout(input_layout)

        # 결과 테이블
        self.table = QTableWidget()
        self.table.setColumnCount(3)
        self.table.setHorizontalHeaderLabels(["Host", "Log Path", "Message"])
        layout.addWidget(self.table)

        self.setLayout(layout)

    def search_logs(self):
        tid = self.tid_input.text().strip()
        if not tid:
            return

        self.table.setRowCount(0)
        all_results = []

        for source in LOG_SOURCES:
            try:
                conn = Connection(
                    host=source["ip"],
                    user=SSH_USER,
                    connect_kwargs={"password": SSH_PASSWORD},
                )
                result = conn.run(f"grep {tid} {source['log_path']}", hide=True, warn=True)
                lines = result.stdout.strip().splitlines()

                for line in lines:
                    all_results.append((source["host_name"], source["log_path"], line))

            except Exception as e:
                all_results.append((source["host_name"], source["log_path"], f"❌ {str(e)}"))

        self.populate_table(all_results)

    def populate_table(self, results):
        self.table.setRowCount(len(results))
        for row, (host, path, message) in enumerate(results):
            self.table.setItem(row, 0, QTableWidgetItem(host))
            self.table.setItem(row, 1, QTableWidgetItem(path))
            msg_item = QTableWidgetItem(message)
            msg_item.setFlags(msg_item.flags() ^ Qt.ItemIsEditable)
            self.table.setItem(row, 2, msg_item)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = LogSearchApp()
    window.show()
    sys.exit(app.exec_())

 

실행 결과 입니다.

만약 오류가 발생하면 ssh 접속이 되는지 확인을 해야합니다.

Posted by 제이브레인
,

Python의 Fabric은 서버에 SSH로 접속해서 원격 명령을 실행하거나 배포 자동화를 할 수 있도록 도와주는 Python 기반의 도구 및 라이브러리입니다. 예전에는 주로 배포 스크립트 자동화 용도로 많이 사용되었고, 최근에는 DevOps나 CI/CD 파이프라인에도 활용되고 있습니다.

1) fabric 설치

pip install fabric

 

설치 후 버전은 확인은 다음과 같습니다.

fab --version

 

Fabric 3.2.2 가 설치되었는데 설치 시점에 따라 버전은 다를 수 있습니다.

(jbdesk38_64) PS D:\repo\jbDeskExample> fab --version
Fabric 3.2.2
Paramiko 3.5.1
Invoke 2.2.0

 

2) fabric 코드 생성

ChatGPT 에 사용한 프롬프트입니다.

python 으로 host_name, user_name, password 를 입력 받아서 os name, version 을 리턴하는 함수를 만들어주세요. 
그리고 host_name, user_name, password 를 파라미터로 받아서 실행하는 예제를 만들어주세요. 
파라미터는  fab_os_info.py -host_name=192.168.56.20 -user_name=vagrant -password=vagrant 와 같이 실행하는 코드를 만들어주세요. 
코드를 만든 후에 테스트 해보고 오류가 없는 코드로 알려주세요.

 

✅ fab_os_info.py (전체 코드)

아래 코드는 다음에서 다운받을 수 있습니다.

git clone git@github.com:jbpark/jbDeskExample.git
cd jbDeskExample/jbDesk/ch3.1

 

import argparse
from fabric import Connection
from invoke.exceptions import UnexpectedExit

def get_os_info(host_name: str, user_name: str, password: str) -> str:
    """
    SSH로 접속해서 OS 이름과 버전을 반환합니다.
    """
    try:
        conn = Connection(
            host=host_name,
            user=user_name,
            connect_kwargs={"password": password}
        )

        try:
            result = conn.run("cat /etc/os-release", hide=True)
            lines = result.stdout.strip().splitlines()
            info = {}
            for line in lines:
                if "=" in line:
                    key, value = line.split("=", 1)
                    info[key.strip()] = value.strip().strip('"')
            return f"{info.get('NAME', 'Unknown OS')} {info.get('VERSION', '')}"

        except UnexpectedExit:
            # /etc/os-release가 없는 경우 (예: CentOS 6)
            result = conn.run("cat /etc/redhat-release", hide=True)
            return result.stdout.strip()

    except Exception as e:
        return f"❌ 접속 실패: {str(e)}"

def main():
    parser = argparse.ArgumentParser(description="Fabric을 사용하여 원격 서버 OS 정보를 가져옵니다.")
    parser.add_argument("-host_name", required=True, help="호스트 이름 또는 IP")
    parser.add_argument("-user_name", required=True, help="SSH 사용자 이름")
    parser.add_argument("-password", required=True, help="SSH 비밀번호")

    args = parser.parse_args()

    os_info = get_os_info(args.host_name, args.user_name, args.password)
    print("✅ OS 정보:", os_info)

if __name__ == "__main__":
    main()

 

✅ 실행 방법

python fab_os_info.py -host_name=192.168.56.20 -user_name=vagrant -password=vagrant

 

3) 테스트 환경 구성

vagrant 로 OS 가 다른 VM 2개를 만들어서 테스트해보겠습니다.

ChatGPT 프롬프트입니다.

Vagrant 로 다음과 같은 환경을 만들어주세요.
그리고 ssh 로그인 가능하게 해주세요.

host_name : rocky9-first
ip : 192.168.56.30
os : rocky9

host_name : ubuntu-second
ip : 192.168.56.31
os : ubuntu

위 환경을 기반으로 Vagrantfile을 아래와 같이 구성했습니다:


✅ 구성 요약

 

VM 이름 P 주소 OS SSH 비밀번호 로그인
rocky9-first 192.168.56.20 Rocky Linux 9 가능 (vagrant/vagrant)
ubuntu-second 192.168.56.21 Ubuntu 22.04 가능 (vagrant/vagrant)

✅ Vagrantfile

Vagrant.configure("2") do |config|
  # 공통 설정
  config.vm.provider "virtualbox" do |vb|
    vb.memory = 1024
    vb.cpus = 1
  end

  # Rocky Linux 9 VM
  config.vm.define "rocky9-first" do |rocky|
    rocky.vm.box = "generic/rocky9"
    rocky.vm.hostname = "rocky9-first"
    rocky.vm.network "private_network", ip: "192.168.56.30"

    rocky.vm.provision "shell", inline: <<-SHELL
      echo "[+] Enabling SSH password login on Rocky Linux..."

      # 비밀번호 로그인 허용
      sed -i 's/^#PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
      sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
      echo 'vagrant:vagrant' | chpasswd
      systemctl restart sshd
    SHELL
  end

  # Ubuntu VM
  config.vm.define "ubuntu-second" do |ubuntu|
    ubuntu.vm.box = "generic/ubuntu2204"
    ubuntu.vm.hostname = "ubuntu-second"
    ubuntu.vm.network "private_network", ip: "192.168.56.31"

    ubuntu.vm.provision "shell", inline: <<-SHELL
      echo "[+] Enabling SSH password login on Ubuntu..."

      sed -i 's/^#PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
      sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
      echo 'vagrant:vagrant' | chpasswd
      systemctl restart sshd
    SHELL
  end
end

✅ 사용 방법

  • 위 코드를 Vagrantfile로 저장
  • VM 생성 및 부팅:
vagrant up
  • SSH 테스트:
ssh vagrant@192.168.56.20  # Rocky
ssh vagrant@192.168.56.21  # Ubuntu
# 비밀번호: vagrant
 
 

또는 Fabric 사용 시:

python fab_os_info.py -host_name=192.168.56.30 -user_name=vagrant -password=vagrant
python fab_os_info.py -host_name=192.168.56.31 -user_name=vagrant -password=vagrant
 
실행 결과는 다음과 같습니다.
(jbdesk38_64) PS D:\repo\jbDeskExample\jbdesk\ch3.1> python fab_os_info.py -host_name 192.168.56.20 -user_name vagrant -password vagrant
✅ OS 정보: Rocky Linux 9.3 (Blue Onyx)
(jbdesk38_64) PS D:\repo\jbDeskExample\jbdesk\ch3.1> python fab_os_info.py -host_name 192.168.56.21 -user_name vagrant -password vagrant
✅ OS 정보: Ubuntu 22.04.3 LTS (Jammy Jellyfish)
 
 

4) 실행 오류

4.1 Bad authentication type

(jbdesk38_64) PS D:\repo\jbDeskExample\jbdesk\ch3.1> python fab_os_info.py -host_name 192.168.56.20 -user_name vagrant -password vagrant
✅ OS 정보: ❌ 접속 실패: Bad authentication type; allowed types: ['publickey', 'gssapi-keyex', 'gssapi-with-mic']

 

이 오류는 SSH 서버가 비밀번호 인증을 아예 허용하지 않고, 공개키 인증만 허용할 때 발생합니다.

✅ 해결 방법 1: 서버에서 비밀번호 인증 허용

⚠️ 보안상 실서비스에서는 권장하지 않지만, 테스트 환경에서는 가능

 

- 서버에 접속 (이미 공개키로 접속 가능한 상태여야 함)

- sshd_config 수정:

sudo nano /etc/ssh/sshd_config

아래 항목 변경:

PasswordAuthentication yes

- SSH 서버 재시작:

 
sudo systemctl restart sshd
Posted by 제이브레인
,

Multi-tenant 로 MariaDB 검색 예제를 만들어보겠습니다.

1) Multi-tenant 설계

회사에서 개발을 할 때 Environment 를 Live, Stage, Dev 로 나누는 경우가 많습니다.

✅ 1.1 Dev (Development)

개발 환경

  • 개발자들이 새로운 기능을 개발하거나 테스트하는 환경
  • 버그가 많을 수 있음 (개발 중이기 때문에)
  • 실제 데이터가 아닌 모의 데이터(mock data) 사용
  • 자주 수정되고 재시작됨

🧪 예시:

"로그인 기능 새로 개발했는데 dev 환경에 올려볼게."


✅ 1.2 Stage (Staging)

사전 검증 환경 (실제 배포 전 테스트용)

  • 운영 환경과 거의 동일한 구성 (서버, DB, 설정 등)
  • QA 팀이 기능을 점검하거나 PM이 검토할 때 사용
  • 실제 데이터와 유사한 테스트 데이터 사용
  • 릴리스 전 최종 점검 단계

🧪 예시:

"이번 배포 전에 stage 환경에서 한번 더 테스트해보자."


✅ 1.3 Live (Production)

실제 서비스 운영 환경

  • 유저들이 직접 사용하는 실제 서비스
  • 장애 발생 시 비즈니스에 바로 영향을 줌 ⚠️
  • 코드와 데이터 모두 신중히 다뤄야 함
  • 실시간 트래픽, 진짜 사용자 데이터

그리고 각 Environment 를 first, second,  ... 와 같이 db_type 으로 나누기도 합니다.

이는 각 고객이나 서비스를 type 별로 구별하여 좀더 세분화 하기 위해서 입니다.

이를 scale out 이라고 합니다. 

scale out 이 scale up 보다 장점이 많이 존재합니다.

  설명
Scale-Up 더 좋은 서버로 교체 → CPU, 메모리, 디스크 용량을 늘리는 방식
Scale-Out 서버 개수를 늘려 부하를 분산하는 방식 (서버 여러 대 운영)

✅ db_type으로 나누는 Scale-Out의 장점

항목 설명
💥 성능 분산 예: first는 고객 주문 DB, second는 상품 관리 DB → 각각의 부하가 분리됨 → 하나가 느려도 다른 DB에는 영향 없음
🛠 유지보수 유연성 특정 DB만 백업/복구, 튜닝, 재시작 가능 → 전체 다운 없이 부분만 점검 가능
🚀 수평 확장성 VM, 컨테이너 등 개별 인스턴스를 필요에 따라 더 추가하기 쉬움
🔐 보안/권한 분리 업무 또는 서비스별로 DB 접근 권한을 다르게 설정할 수 있음
🧪 테스트 분리 용이 개발자가 특정 db_type 만 테스트하거나 복제해서 실험 가능
💸 비용 최적화 필요한 DB만 리소스를 늘릴 수 있어, 전체 고성능 서버보다 비용 효율적

❌ Scale-Up 방식의 한계

한계 설명
단일 장애 지점 (SPOF) 모든 DB가 하나의 인스턴스에 있으면, 다운 시 전체 서비스 영향
리소스 한계 아무리 좋은 서버라도 CPU, RAM은 물리적 한계가 있음
운영 부담 증가 하나의 DB에 모든 기능이 몰리면 관리/튜닝이 어려워짐

 

🎯 예시: multi-tenant + scale-out 구조

예를 들면 아래와 같이 first DB 는 주문 처리를 하고 second DB 는 상품 관리를 하는 것과 같이 분리할 수 있습니다.

 

다음은 전체 설계 내용입니다.

Environment 로 Lvie(상용 서버 배포), Stage(상용 배포전 사전 검증), Dev(개발 서버) 로 구분하였습니다.

db_type 으로 first, second 를 분리하였습니다.

ChatGPT 에 사용한 프롬프트입니다.

vagrant 로 rocky 9 OS에 mariadb VM 6개를 만들려고 합니다. 
mariadb 는 password vagrant 로 접속 가능하게 해주세요.
외부에서 3306 포트로 접속이 가능하게 해주세요.
sample db 도 만들고 sample table 도 생성하고 sample data 도 추가해주세요.

multi-tenant 로 environment, db_type 으로 구분하려고 합니다.
environment 는 Live, Stage, Dev 로 나눌 예정이며
db_type 은 first, second 로 나눌 예정입니다.
VM 이름과 IP 는 다음과 같습니다.
dev_first : 192.168.56.20
dev_second : 192.168.56.21
stage_first : 192.168.56.22
stage_second : 192.168.56.23
live_first : 192.168.56.24
live_second : 192.168.56.25

python 으로 environment, db_type 이라는 파라미터 입력을 받아서
각 VM 에 설치된 mariadb 의 sample table 을 검색하는 예제를 만들어주세요.
first 에는 sample table 로 주문 처리 관련 테이블을 생성하고 sample 데이터도 넣어주세요.
second 에는 sample table 로 상품 관리 테이블을 생성하고 sample 데이터도 넣어주세요.

 

생성된 Vagrantfile 입니다.

아래 파일은 다음 git 에서 다운로드 가능합니다.

git clone git@github.com:jbpark/jbDeskExample.git
cd jbDeskExample/jbDesk/ch2.3

 

✅ 1) VM 구성 (Vagrantfile 예시)

 
VAGRANT_BOX = "generic/rocky9"
DB_PASSWORD = "vagrant"
MARIADB_PORT = 3306

VMs = {
  "dev-first"    => "192.168.56.20",
  "dev-second"   => "192.168.56.21",
  "stage-first"  => "192.168.56.22",
  "stage-second" => "192.168.56.23",
  "live-first"   => "192.168.56.24",
  "live-second"  => "192.168.56.25"
}

Vagrant.configure("2") do |config|
  config.vm.box = VAGRANT_BOX

  VMs.each do |name, ip|
    config.vm.define name do |vm_config|
      vm_config.vm.hostname = name
      vm_config.vm.network "private_network", ip: ip
      vm_config.vm.provider "virtualbox" do |vb|
        vb.name = name
        vb.memory = 1024
        vb.cpus = 1
      end

      # Determine table type based on VM name
      table_sql = if name.include?("first")
        <<-SQL
          CREATE TABLE IF NOT EXISTS orders (
              id INT AUTO_INCREMENT PRIMARY KEY,
              customer_name VARCHAR(100),
              product VARCHAR(100),
              quantity INT,
              order_date DATE
          );

          INSERT IGNORE INTO orders (customer_name, product, quantity, order_date)
          VALUES ('John Doe', 'Laptop', 1, '2025-04-01'),
                 ('Jane Smith', 'Monitor', 2, '2025-04-02');
        SQL
      else
        <<-SQL
          CREATE TABLE IF NOT EXISTS products (
              id INT AUTO_INCREMENT PRIMARY KEY,
              name VARCHAR(100),
              price INT,
              stock INT
          );

          INSERT IGNORE INTO products (name, price, stock)
          VALUES ('Laptop', 1200000, 10),
                 ('Monitor', 300000, 25);
        SQL
      end

      vm_config.vm.provision "shell", inline: <<-SHELL
        dnf install -y mariadb-server
        systemctl enable mariadb
        systemctl start mariadb

        echo "[+] Configuring MariaDB to allow remote access..."

        # bind-address=0.0.0.0 설정
        sed -i 's/^bind-address=127.0.0.1/bind-address=0.0.0.0/' /etc/my.cnf.d/mariadb-server.cnf || \
        echo -e "[mysqld]\nbind-address=0.0.0.0" >> /etc/my.cnf.d/mariadb-server.cnf

        systemctl restart mariadb

        echo "[+] Creating remote-accessible root user..."
        mariadb -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'vagrant' WITH GRANT OPTION;"
        mariadb -e "FLUSH PRIVILEGES;"

        # Create sample DB and appropriate table
        mysql -uroot -p#{DB_PASSWORD} <<EOF
CREATE DATABASE IF NOT EXISTS sample;
USE sample;
#{table_sql}
EOF

        echo "[+] Opening firewall for MariaDB..."
        firewall-cmd --permanent --add-port=3306/tcp
        firewall-cmd --reload
      SHELL
    end
  end
end

 

📝 실행 방법

vagrant up
 

✅ 2) Python 예제 (query_sample_table.py)

 
import pymysql

# VM 정보 매핑
VM_INFO = {
    "dev_first":    {"ip": "192.168.56.20"},
    "dev_second":   {"ip": "192.168.56.21"},
    "stage_first":  {"ip": "192.168.56.22"},
    "stage_second": {"ip": "192.168.56.23"},
    "live_first":   {"ip": "192.168.56.24"},
    "live_second":  {"ip": "192.168.56.25"},
}

def get_vm_name(environment: str, db_type: str) -> str:
    return f"{environment.lower()}_{db_type.lower()}"

def table_exists(cursor, table_name: str) -> bool:
    cursor.execute(f"SHOW TABLES LIKE '{table_name}';")
    return cursor.fetchone() is not None

def query_sample_tables(ip: str, user: str = "root", password: str = "vagrant", db: str = "sample"):
    try:
        conn = pymysql.connect(host=ip, port=3306, user=user, password=password, database=db)
        cursor = conn.cursor()

        if table_exists(cursor, "orders"):
            print("[orders 테이블]")
            cursor.execute("SELECT * FROM orders;")
            for row in cursor.fetchall():
                print(row)
        else:
            print("[orders 테이블 없음]")

        if table_exists(cursor, "products"):
            print("\n[products 테이블]")
            cursor.execute("SELECT * FROM products;")
            for row in cursor.fetchall():
                print(row)
        else:
            print("[products 테이블 없음]")

        cursor.close()
        conn.close()
    except Exception as e:
        print(f"[!] 접속 실패: {e}")

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument("--environment", choices=["dev", "stage", "live"], required=True)
    parser.add_argument("--db_type", choices=["first", "second"], required=True)
    args = parser.parse_args()

    vm_name = get_vm_name(args.environment, args.db_type)
    ip = VM_INFO.get(vm_name, {}).get("ip")

    if not ip:
        print(f"[!] VM 정보 없음: {vm_name}")
    else:
        print(f"[+] {vm_name} ({ip}) 에 접속합니다...")
        query_sample_tables(ip)

 

💻 실행 예시

 
python query_sample_table.py --environment dev --db_type first

 

위의 경우 environment 는 dev 를 db_type 은 first 로 쿼리 요청을 하면 host 는 dev-first 에 있는 mariadb 검색 요청을 합니다.

 

3) JbDesk 와 통합

통합된 코드는 다음에서 다운받을 수 있습니다.

Env 에서 environment 는 dev 를 db_type 은 first 로 쿼리 요청 시 dev-first 의 MariaDb 에서 검색 요청이 이루어집니다.

git clone git@github.com:jbpark/jbDesk.git
cd jbDesk

Posted by 제이브레인
,

Vagrant 로 Rocky Linux 에 MariaDB 를 설치해보겠습니다.

이번에는 VM 3개에 MariaDB 를 설치하여 Multi-Tenant 를 구성해보겠습니다.

Multi-Tenant 는 단일 인스턴스가 여러 고객에게 서비스를 제공하는 아키텍처를 말합니다.

이제 MariaDB가 각기 다른 테넌트의 데이터베이스를 처리하도록 설정되었습니다. 이를 통해 여러 애플리케이션이 각기 다른 데이터베이스에 연결하여 독립적으로 작업할 수 있습니다. 예를 들어, 애플리케이션은 다음과 같이 각 MariaDB 인스턴스에 연결할 수 있습니다.

테넌트 1: mariadb1:3306

테넌트 2: mariadb2:3307

테넌트 3: mariadb3:3308

각 애플리케이션은 테넌트마다 고유한 데이터베이스에 연결하여 데이터를 처리하게 됩니다.

 

ChatGPT 에 사용한 프롬프트입니다

vagrant 로 virtualbox 에 rocky linux 9 에 mariadb 를 설치하는 VM 3개를 만들어주세요.
VM 의 IP 는 192.168.56.20, 192.168.56.21, 192.168.56.22 로 설정하고 
VM 이름은 maridb20, maridb21, maridb22 으로 설치하는 방법을 단계별로 알려주세요.
mariadb 는 password vagrant 로 접속 가능하게 해주세요.
외부에서 3306 포트로 접속이 가능하게 해주세요.
sample db 도 만들고 sample table 도 생성하고 sample data 도 추가해주세요.

 

1) Vagrant 설치

Vagrant 1.1편-Vagrant설치 를 참고해주세요.

2) 프로젝트 폴더 생성

프로젝트 폴더를 만들어주어야 합니다.

mkdir vagrant-rocky-mariadb
cd vagrant-rocky-mariadb

 

3) Vagrantfile 생성

생성된 Vagrantfile 입니다.

다음 파일은 아래 git 에서 다운받을 수 있습니다.

git clone git@github.com:jbpark/jbDeskExample.git
cd jbDeskExample/vagrant/ch1.3

 

프로젝트 폴더에 아래 Vagrnatfile 을 생성해주세요.

Vagrantfile

Vagrant.configure("2") do |config|
  nodes = [
    { :name => "mariadb20", :ip => "192.168.56.20" },
    { :name => "mariadb21", :ip => "192.168.56.21" },
    { :name => "mariadb22", :ip => "192.168.56.22" }
  ]

  nodes.each do |node|
    config.vm.define node[:name] do |node_config|
      node_config.vm.box = "rockylinux/9"
      node_config.vm.hostname = node[:name]
      node_config.vm.network "private_network", ip: node[:ip]

      node_config.vm.provider "virtualbox" do |vb|
        vb.name = node[:name]
        vb.memory = 1024
        vb.cpus = 1
      end

      node_config.vm.network "forwarded_port", guest: 3306, host: 3306, auto_correct: true

      node_config.vm.provision "shell", inline: <<-SHELL
        dnf install -y mariadb-server
        systemctl enable mariadb
        systemctl start mariadb

        echo "[+] Configuring MariaDB to allow remote access..."

        # bind-address=0.0.0.0 설정
        sed -i 's/^bind-address=127.0.0.1/bind-address=0.0.0.0/' /etc/my.cnf.d/mariadb-server.cnf || \
        echo -e "[mysqld]\nbind-address=0.0.0.0" >> /etc/my.cnf.d/mariadb-server.cnf

        systemctl restart mariadb

        echo "[+] Creating remote-accessible root user..."
        mariadb -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'vagrant' WITH GRANT OPTION;"
        mariadb -e "FLUSH PRIVILEGES;"

        echo "[+] Creating sample DB, tables, and inserting sample data..."
        mariadb -u root <<EOF
CREATE DATABASE IF NOT EXISTS sample;
USE sample;

CREATE TABLE IF NOT EXISTS users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100),
    email VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS products (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100),
    price DECIMAL(10, 2),
    stock INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO users (name, email) VALUES
    ('Alice', 'alice@example.com'),
    ('Bob', 'bob@example.com'),
    ('Charlie', 'charlie@example.com');

INSERT INTO products (name, price, stock) VALUES
    ('Laptop', 1200.00, 10),
    ('Smartphone', 800.00, 25),
    ('Headphones', 150.00, 50);
EOF

        echo "[+] Opening firewall for MariaDB..."
        firewall-cmd --permanent --add-port=3306/tcp
        firewall-cmd --reload
      SHELL
    end
  end
end

 

4) VM 생성 및 구동

vagrant up

 

5) VM 접속

각 VM 에는 아래와 같이 접속이 가능합니다.

vagrant ssh mariadb20
vagrant ssh mariadb21
vagrant ssh mariadb22

 

6) MariaDb 접속

Dbeaver 로는 Database Client 프로그램으로 접속해보겠습니다.

✅ 1. DBeaver 다운로드 및 설치

  1. 공식 홈페이지 방문
    👉 https://dbeaver.io/download/
  2. OS에 맞는 설치파일 다운로드
    • Windows: DBeaver Community Edition Windows Installer
    • Mac: .dmg
    • Linux: .deb 또는 .rpm
  3. 설치 프로그램 실행 후, 기본 설정으로 설치 진행

✅ 2. MariaDB 접속 정보 확인

MariaDB 서버가 로컬 Vagrant VM에서 구동 중이라면 다음과 같이 확인하세요:

항목
Host 192.168.56.20 또는 192.168.56.21 또는 192.168.56.22
Port 3306
User root
Password vagrant
Database sample

※ vagrantfile에서 root 사용자의 비밀번호를 vagrant로 설정한 경우입니다.


✅ 3. DBeaver에서 MariaDB 접속

  1. DBeaver 실행 후 왼쪽 상단 [Database] → [New Database Connection] 클릭
    또는 단축키 Ctrl + N
  2. 검색창에 "MariaDB" 입력 → MariaDB 선택 → Next
  3. 다음 정보를 입력:
  4. yaml
    복사편집
    Host: 127.0.0.1 Port: 3306 Database: sample Username: root Password: vagrant
  5. [Test Connection] 버튼 클릭 → 정상 연결되면 성공
  6. Finish 버튼을 눌러 접속 완료

192.168.56.22 에 접속한 예입니다.

 


✅ 4. 접속 확인

  • 왼쪽 탐색창에서 sample DB 아래 tables 를 확장하면
    users, products 테이블을 볼 수 있습니다.
  • 마우스 우클릭 → View Data → View All Rows 선택 시 데이터 조회 가능

'유틸리티 > Vagrant' 카테고리의 다른 글

Vagrant 1.2편-Rocky Linux 에 Oracle 설치  (2) 2025.03.31
Vagrant 1.1편-Vagrant설치  (1) 2025.03.30
Posted by 제이브레인
,

Oracle DB 연결 방법을 알아보겠습니다.

1) Python 패키지 설치

sqlalchemy, cx_Oracle 라이브러리 두개를 설치해야합니다.

pip install sqlalchemy
pip install cx_Oracle

 

2) Instant Client 설치

cx_Oracle을 사용하려면 Oracle Instant Client가 필요합니다. Windows 환경에서 설치하는 방법은 다음과 같습니다.

2.1. Instant Client 다운로드

  1. Oracle 공식 사이트에서 Windows용 Instant Client를 다운로드합니다.
  2. Instant Client Basic Package (ZIP 파일) 을 다운로드합니다.
  3. 다운로드한 ZIP 파일을 적절한 폴더(C:\oracle\instantclient_23_7 등)에 압축 해제합니다. (파일명은 선택한 버전에 따라 달라질 수  있습니다.)

본인 OS 버전에 맞는 Client 를 선택합니다.
설치할 버전을 선택합니다.
Basic Package 를 선택합니다.

2.2. 환경 변수 설정

  1. 시스템 환경 변수 추가
    • INSTANT_CLIENT 환경 변수를 추가하고 Instant Client 경로를 입력합니다.
    • 예: C:\oracle\instantclient_23_7
  2. Path 변수에 추가
    • C:\oracle\instantclient_23_7 을 Path 환경 변수에 추가합니다.

3) Oracle DB Sample Table 추가

2.1편에서 생성한 Oracle 에 다음 sql 을 사용하여 Sample Table 을 추가하였습니다.

-- EMP 테이블 생성
CREATE TABLE EMP (
    EMPNO NUMBER(4) PRIMARY KEY,
    ENAME VARCHAR2(10),
    JOB VARCHAR2(9),
    MGR NUMBER(4),
    HIREDATE DATE,
    SAL NUMBER(7,2),
    COMM NUMBER(7,2),
    DEPTNO NUMBER(2)
);

-- DEPT 테이블 생성
CREATE TABLE DEPT (
    DEPTNO NUMBER(2) PRIMARY KEY,
    DNAME VARCHAR2(14),
    LOC VARCHAR2(13)
);

-- BONUS 테이블 생성
CREATE TABLE BONUS (
    ENAME VARCHAR2(10),
    JOB VARCHAR2(9),
    SAL NUMBER,
    COMM NUMBER
);

-- SALGRADE 테이블 생성
CREATE TABLE SALGRADE (
    GRADE NUMBER,
    LOSAL NUMBER,
    HISAL NUMBER
);

-- DUMMY 테이블 생성
CREATE TABLE DUMMY (
    DUMMY CHAR(1)
);

-- 데이터 삽입
INSERT INTO DEPT VALUES (10, 'ACCOUNTING', 'NEW YORK');
INSERT INTO DEPT VALUES (20, 'RESEARCH', 'DALLAS');
INSERT INTO DEPT VALUES (30, 'SALES', 'CHICAGO');
INSERT INTO DEPT VALUES (40, 'OPERATIONS', 'BOSTON');

INSERT INTO EMP VALUES (7369, 'SMITH', 'CLERK', 7902, TO_DATE('1980-12-17', 'YYYY-MM-DD'), 800, NULL, 20);
INSERT INTO EMP VALUES (7499, 'ALLEN', 'SALESMAN', 7698, TO_DATE('1981-02-20', 'YYYY-MM-DD'), 1600, 300, 30);
INSERT INTO EMP VALUES (7521, 'WARD', 'SALESMAN', 7698, TO_DATE('1981-02-22', 'YYYY-MM-DD'), 1250, 500, 30);
INSERT INTO EMP VALUES (7566, 'JONES', 'MANAGER', 7839, TO_DATE('1981-04-02', 'YYYY-MM-DD'), 2975, NULL, 20);
INSERT INTO EMP VALUES (7698, 'BLAKE', 'MANAGER', 7839, TO_DATE('1981-05-01', 'YYYY-MM-DD'), 2850, NULL, 30);
INSERT INTO EMP VALUES (7782, 'CLARK', 'MANAGER', 7839, TO_DATE('1981-06-09', 'YYYY-MM-DD'), 2450, NULL, 10);
INSERT INTO EMP VALUES (7788, 'SCOTT', 'ANALYST', 7566, TO_DATE('1987-07-13', 'YYYY-MM-DD'), 3000, NULL, 20);
INSERT INTO EMP VALUES (7839, 'KING', 'PRESIDENT', NULL, TO_DATE('1981-11-17', 'YYYY-MM-DD'), 5000, NULL, 10);
INSERT INTO EMP VALUES (7902, 'FORD', 'ANALYST', 7566, TO_DATE('1981-12-05', 'YYYY-MM-DD'), 3000, NULL, 20);
INSERT INTO EMP VALUES (7934, 'MILLER', 'CLERK', 7782, TO_DATE('1982-01-23', 'YYYY-MM-DD'), 1300, NULL, 10);

INSERT INTO SALGRADE VALUES (1, 700, 1200);
INSERT INTO SALGRADE VALUES (2, 1201, 1400);
INSERT INTO SALGRADE VALUES (3, 1401, 2000);
INSERT INTO SALGRADE VALUES (4, 2001, 3000);
INSERT INTO SALGRADE VALUES (5, 3001, 9999);

INSERT INTO DUMMY VALUES ('X');

COMMIT;

 

4) Python에서 cx_Oracle 사용하기

import cx_Oracle

# Oracle Instant Client가 올바르게 설정되었는지 확인
print(cx_Oracle.clientversion())

# Oracle 데이터베이스 연결
dsn = cx_Oracle.makedsn("your_host", 1521, service_name="your_service_name")
connection = cx_Oracle.connect(user="your_username", password="your_password", dsn=dsn)

# 커서 생성
cursor = connection.cursor()

# 데이터 조회 예제
cursor.execute("SELECT * FROM your_table")
for row in cursor.fetchall():
    print(row)

# 리소스 해제
cursor.close()
connection.close()

 

2.1편에서 생성한 Oracle 설정에 맞추어 다음과 같이 수정하였습니다.

connect_oracle_table.py

import cx_Oracle

# Oracle Instant Client가 올바르게 설정되었는지 확인
print(cx_Oracle.clientversion())

# Oracle 데이터베이스 연결
dsn = cx_Oracle.makedsn("192.168.56.10", 1521, service_name="XEPDB1")
connection = cx_Oracle.connect(user="testuser", password="test1234", dsn=dsn)

# 커서 생성
cursor = connection.cursor()

# 데이터 조회 예제
cursor.execute("SELECT * FROM EMP")
for row in cursor.fetchall():
    print(row)

# 리소스 해제
cursor.close()
connection.close()

 

실행 결과는 다음과 같습니다.

EMP 테이블의 데이터가 정상적으로 검색됨을 확인 할 수 있습니다.

insert_sample.sql

(23, 7, 0, 25, 1)
(7369, 'SMITH', 'CLERK', 7902, datetime.datetime(1980, 12, 17, 0, 0), 800.0, None, 20)
(7499, 'ALLEN', 'SALESMAN', 7698, datetime.datetime(1981, 2, 20, 0, 0), 1600.0, 300.0, 30)
(7521, 'WARD', 'SALESMAN', 7698, datetime.datetime(1981, 2, 22, 0, 0), 1250.0, 500.0, 30)
(7566, 'JONES', 'MANAGER', 7839, datetime.datetime(1981, 4, 2, 0, 0), 2975.0, None, 20)
(7698, 'BLAKE', 'MANAGER', 7839, datetime.datetime(1981, 5, 1, 0, 0), 2850.0, None, 30)
(7782, 'CLARK', 'MANAGER', 7839, datetime.datetime(1981, 6, 9, 0, 0), 2450.0, None, 10)
(7788, 'SCOTT', 'ANALYST', 7566, datetime.datetime(1987, 7, 13, 0, 0), 3000.0, None, 20)
(7839, 'KING', 'PRESIDENT', None, datetime.datetime(1981, 11, 17, 0, 0), 5000.0, None, 10)
(7902, 'FORD', 'ANALYST', 7566, datetime.datetime(1981, 12, 5, 0, 0), 3000.0, None, 20)
(7934, 'MILLER', 'CLERK', 7782, datetime.datetime(1982, 1, 23, 0, 0), 1300.0, None, 10)

 

5) Oracle 검색 후 결과를 Qt 에 출력

이번에는 좀더 복잡한 Pyqt5 에 이름을 검색하여 결과를 출력하는 예제를 만들어보겠습니다.

ChatGPT에 사용한 프롬프트입니다.

pyqt5 로 실행 시 window 에 다음을 추가해주세요.
첫째 라인 : EMP Name 라는 GroupBox 로 둘러싸인 LineEdit, Search 버튼
둘째 라인 : Name, Job 이라는 Column 을 가지는 Grid

Oracle 정보는 다음과 같습니다.
host : 192.168.56.10
service name : XEPDB1
user : testuser
password : test1234

EMP Table 은 다음과 같습니다.

CREATE TABLE EMP (
    EMPNO NUMBER(4) PRIMARY KEY,
    ENAME VARCHAR2(10),
    JOB VARCHAR2(9),
    MGR NUMBER(4),
    HIREDATE DATE,
    SAL NUMBER(7,2),
    COMM NUMBER(7,2),
    DEPTNO NUMBER(2)
);

Seach 버튼을 클릭하면 cx_Oracle 로 EMP 테이블을 검색하여 EMP Name LineEdit 에 입력된 값으로 검색하여 Name 이 일치하는 
레코드를 찾습니다.
일치하는 데이터를 찾으면 Grid 에 Name, Job 를 추가합니다.

 

생성된 코드입니다.

아래 코드는 다음 git 에서도 다운받을 수 있습니다.

git clone git@github.com:jbpark/jbDeskExample.git
cd jbDeskExample/jbDesk/ch2.2

 

search_oracle_table.py

import sys
import cx_Oracle
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QGroupBox, QLineEdit, QPushButton,
    QTableWidget, QTableWidgetItem, QHBoxLayout
)


class EmployeeSearchApp(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        layout = QVBoxLayout()

        # EMP Name GroupBox
        self.groupBox = QGroupBox("EMP Name")
        groupBoxLayout = QHBoxLayout()

        self.searchInput = QLineEdit()
        self.searchButton = QPushButton("Search")
        self.searchButton.clicked.connect(self.searchEmployee)

        groupBoxLayout.addWidget(self.searchInput)
        groupBoxLayout.addWidget(self.searchButton)
        self.groupBox.setLayout(groupBoxLayout)

        # 결과 테이블
        self.resultTable = QTableWidget()
        self.resultTable.setColumnCount(2)
        self.resultTable.setHorizontalHeaderLabels(["Name", "Job"])

        layout.addWidget(self.groupBox)
        layout.addWidget(self.resultTable)

        self.setLayout(layout)
        self.setWindowTitle("Employee Search")
        self.resize(400, 300)

    def searchEmployee(self):
        emp_name = self.searchInput.text().strip()
        if not emp_name:
            return

        # Oracle DB 연결 정보
        dsn = cx_Oracle.makedsn("192.168.56.10", 1521, service_name="XEPDB1")
        conn = cx_Oracle.connect(user="testuser", password="test1234", dsn=dsn)
        cursor = conn.cursor()

        # EMP 테이블 검색
        query = """
        SELECT ENAME, JOB FROM EMP WHERE ENAME = :ename
        """
        cursor.execute(query, ename=emp_name)

        results = cursor.fetchall()

        # 테이블에 데이터 채우기
        self.resultTable.setRowCount(len(results))
        for row_idx, row_data in enumerate(results):
            self.resultTable.setItem(row_idx, 0, QTableWidgetItem(row_data[0]))
            self.resultTable.setItem(row_idx, 1, QTableWidgetItem(row_data[1]))

        cursor.close()
        conn.close()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = EmployeeSearchApp()
    window.show()
    sys.exit(app.exec_())

 

실행 결과는 다음과 같습니다.

쉽지 않나요?

ChatGPT 에 연동에 필요한 정보만 정확하게 알려주면 바로 바로 실행할 수 있는 코드를 알려주네요.

세상 참 좋아졌어요.

ChatGPT 등의 인공지능을 사용하여 코딩하는 세대를 AI Native 세대라고 하더라구요.

정말 인공지능은 선택이 아닌 필수인 것 같습니다.

 

6) JbDesk 와 통합

JbDesk 에 Oracle 연동 메뉴도 통합하였습니다.

관련 코드는 다음 git 에서 다운받을 수 있습니다.

git clone git@github.com:jbpark/jbDeskExample.git
cd jbDeskExample/jbDesk/ch2.2

JbDesk.py

Posted by 제이브레인
,

 

Vagrant 를 사용하면 복잡하게 Oracle XE 다운받고 설정할 필요없이 간단하게 설치가 가능합니다.

ChatGPT 에 사용한 프롬프트입니다

vagrant 로 virtualbox 에 rocky linux 9 에 oracle 개인 무료 버전 21c를 설치하고 
VM 의 IP 는 192.168.56.10 로 설정하고 
VM 이름은 oracle21c 으로 설치하는 방법을 단계별로 알려주세요. 
disksize 는 20GB 로 늘려주세요. 메모리는 4GB 로 할당해주세요.

전체적으로 다음과 같은 과정으로 진행이 됩니다.

- Vagrant plugin 설치

- Vagrant 프로젝트 폴더 만들기

- Vagrantfile 생성

- Vagrant 실행 (vagrant up)

1) Vagrant Plugin 설치

Vagrant 는 기본적으로 저장공간은 10GB 입니다.

Oracle 은 최소 20GB 를 필요로하기 때문에 disk size 를 20GB로 늘렸습니다.

Vagrant 에서 disk size 를 늘리기 위해 disksize plugin 이 필요합니다.

다음과 같이 plugin 을 설치합니다.

vagrant plugin install vagrant-disksize

 

2) Vagrant 프로젝트 폴더 만들기

프로젝터 폴더를 생성합니다.

폴더 이름은 본인이 원하는 이름을 사용해도 됩니다.

mkdir vagrant-rocky-oracle-xe
cd vagrant-rocky-oracle-xe
vagrant init generic/rocky9

generic/rocky9는 Rocky Linux 9을 기반으로 하는 Vagrant Box입니다.

Vagrantfile 을 아래와 같이 수정합니다.

4GB 메모리로 oracle 21c 설치하는 예제입니다.

아래 코드는 다음에서 다운로드가 가능합니다.

git clone git@github.com:jbpark/jbDeskExample.git
cd jbDeskExample/vagrant/ch1.2

Vagrantfile

Vagrant.configure("2") do |config|
  # ✅ Rocky Linux 9 사용
  config.vm.box = "rockylinux/9"
  config.disksize.size = '20GB'

  # VM 이름 설정
  config.vm.define "oracle21c" do |oracle|
    oracle.vm.hostname = "oracle21c"

    # 네트워크 설정 (고정 IP)
    oracle.vm.network "private_network", ip: "192.168.56.10"

    # VirtualBox 설정
    oracle.vm.provider "virtualbox" do |vb|
      vb.name = "oracle21c"
      vb.memory = "4096"
      vb.cpus = 2
    end

    config.vm.synced_folder ".", "/vagrant", disabled: true

    # 프로비저닝 (Oracle 21c XE 설치)
    oracle.vm.provision "shell", inline: <<-SHELL
      echo "=====[ 1. resize disk ]====="
      sudo yum clean all
      sudo yum -y update
      sudo yum install cloud-utils-growpart -y
      sudo growpart /dev/sda 4
      sudo xfs_growfs /
      echo "=====[ 2. 패키지 업데이트 및 필수 라이브러리 설치 ]====="
      sudo yum -y install wget
      sudo systemctl stop firewalld
      sudo systemctl disable firewalld
      sudo systemctl status firewalld

      # 의존 패키지 설치
      sudo wget https://dl.rockylinux.org/pub/rocky/8/AppStream/x86_64/os/Packages/c/compat-openssl10-1.0.2o-4.el8_6.x86_64.rpm
      sudo dnf -y localinstall compat-openssl10-1.0.2o-4.el8_6.x86_64.rpm

      echo "=====[ 3. Oracle 설치 ]====="
      # 사전 설치 파일 다운로드
      sudo curl -o oracle-database-preinstall-21c-1.0-1.el8.x86_64.rpm https://yum.oracle.com/repo/OracleLinux/OL8/appstream/x86_64/getPackage/oracle-database-preinstall-21c-1.0-1.el8.x86_64.rpm
	  sudo dnf -y localinstall oracle-database-preinstall-21c*

      # 설치 파일 실행
      # -L 옵션 : Redirect (301, 302)가 응답될 경우, redirect 된 링크로 가서, 파일을 다운로드 받음
      sudo curl -L -o oracle-database-xe-21c-1.0-1.ol8.x86_64.rpm https://download.oracle.com/otn-pub/otn_software/db-express/oracle-database-xe-21c-1.0-1.ol8.x86_64.rpm
      sudo dnf -y localinstall oracle-database-xe-21c*

      # rpm 파일 삭제
      sudo rm -rf compat-openssl10-1.0.2o-4.el8_6.x86_64.rpm
      sudo rm -rf oracle-database-preinstall-21c-1.0-1.el8.x86_64.rpm
      sudo rm -rf oracle-database-xe-21c-1.0-1.ol8.x86_64.rpm

      export CV_ASSUME_DISTID=RHEL8
      export ORACLE_BASE=/opt/oracle
      export ORACLE_HOME=/opt/oracle/product/21c/dbhomeXE
      export ORACLE_SID=XE
      export PATH=/opt/oracle/product/21c/dbhomeXE/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:\$PATH

      sudo echo "========== Setting up environment variables =========="
      sudo echo "export CV_ASSUME_DISTID=RHEL8" | sudo tee -a /etc/environment
      sudo echo "export ORACLE_BASE=/opt/oracle" | sudo tee -a /etc/environment
      sudo echo "export ORACLE_HOME=/opt/oracle/product/21c/dbhomeXE" | sudo tee -a /etc/environment
      sudo echo "export PATH=/opt/oracle/product/21c/dbhomeXE/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:\$PATH" | sudo tee -a /etc/environment
      sudo echo "export ORACLE_SID=XE" | sudo tee -a /etc/environment
      source /etc/environment

      echo "========== Removing old listener config if exists =========="
      sudo rm -rf /opt/oracle/homes/OraDB21Home1/network/admin/listener.ora

      echo "Updating /etc/hosts..."
      sudo bash -c 'echo "192.168.56.10 oracle21c" >> /etc/hosts'

      echo "========== Configuring Oracle XE with auto password =========="
      printf "Oracle1234\nOracle1234\n" | sudo /etc/init.d/oracle-xe-21c configure

      echo "========== Updating listener.ora =========="
      cat <<EOL | sudo tee /opt/oracle/homes/OraDBHome21cXE/network/admin/listener.ora
DEFAULT_SERVICE_LISTENER = XE

LISTENER =
(DESCRIPTION_LIST =
  (DESCRIPTION =
    (ADDRESS = (PROTOCOL = TCP)(HOST = 0.0.0.0)(PORT = 1521))
    (ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC1521))
  )
)
EOL

      sudo chown -R oracle:oinstall /opt/oracle
      sudo chmod -R 750 /opt/oracle
      sudo chmod 640 /opt/oracle/homes/OraDBHome21cXE/network/admin/listener.ora

      echo "========== Restarting Oracle Listener =========="
      sudo -u oracle $ORACLE_HOME/bin/lsnrctl stop
      sudo -u oracle $ORACLE_HOME/bin/lsnrctl start

      echo "========== Enabling and starting Oracle service =========="
      sudo systemctl enable oracle-xe-21c
      sudo systemctl restart oracle-xe-21c

      echo "========== Checking Oracle service status =========="
      sudo systemctl status oracle-xe-21c --no-pager

      echo "========== Oracle XE 21c Installation Completed! =========="

      # 사용자 설정
      ORACLE_USER="testuser"      # 생성할 계정명
      ORACLE_PASSWORD="test1234"  # 계정 비밀번호
      ORACLE_SID="XE"             # DB 서비스명 (XE 기본값)

      # SQL 실행 함수
      create_user_sql=$(cat <<EOF
ALTER SESSION SET CONTAINER = XEPDB1;
CREATE USER $ORACLE_USER IDENTIFIED BY "$ORACLE_PASSWORD";
GRANT CONNECT, RESOURCE TO $ORACLE_USER;
ALTER USER $ORACLE_USER QUOTA UNLIMITED ON USERS;
GRANT CREATE SESSION TO $ORACLE_USER;
ALTER PLUGGABLE DATABASE XEPDB1 OPEN;
ALTER PLUGGABLE DATABASE XEPDB1 SAVE STATE;
EXIT;
EOF
      )

      # SQL 실행 (sysdba 권한 필요)
      echo "Oracle 계정 생성 중: $ORACLE_USER"
      echo "$create_user_sql" | sudo -u oracle /opt/oracle/product/21c/dbhomeXE/bin/sqlplus -s / as sysdba

      echo "Oracle 계정 $ORACLE_USER 생성 완료!"
    SHELL
  end
end

 

 

 

3) Vagrant up

 

아래 명령으로 VM 생성을 합니다.

vagrant up

vagrant 생성 이나 삭제 시에는 vagrant 에서 VM 을 종료 시키거나 VM 파일을 삭제해야할 수 있습니다.

그래서 이러한 과정을 처리하는 스크립트를 만들었습니다.

VM 생성

create_vm.bat

@echo off
set VM_NAME=oracle21c
set VM_PATH=%USERPROFILE%\VirtualBox VMs\%VM_NAME%
set VDI_FILE=%VM_NAME%.vdi

:: VirtualBox의 전체 경로로 VBoxManage 실행
set VBOXMANAGE="C:\Program Files\Oracle\VirtualBox\VBoxManage.exe"

if exist "%VDI_FILE%" (
    %VBOXMANAGE% closemedium disk "%VDI_FILE%"
    del /f /q "%VDI_FILE%"
    echo %VDI_FILE% has been deleted.
) else (
    echo %VDI_FILE% does not exist.
)

if exist "%VM_PATH%" (
    echo %VM_NAME% already exists.
) else (
    echo %VM_NAME% does not exist. Starting Vagrant VM...
    vagrant halt
    vagrant up
)

 

VM 삭제

remove_vm.bat

@echo off
set VM_NAME=oracle21c
set VM_PATH=%USERPROFILE%\VirtualBox VMs\%VM_NAME%
set VDI_FILE=%VM_NAME%.vdi

:: VirtualBox의 전체 경로로 VBoxManage 실행
set VBOXMANAGE="C:\Program Files\Oracle\VirtualBox\VBoxManage.exe"

:: VM이 실행 중인지 확인하고 종료
echo Checking if VM is running...
%VBOXMANAGE% showvminfo %VM_NAME% --machinereadable | findstr "VMState=" | findstr /i "running"
if %errorlevel%==0 (
    echo VM is running. Stopping the VM...
    vagrant halt %VM_NAME% --force

    :: 잠시 기다린 후 삭제 작업
    timeout /t 5 /nobreak
) else (
    echo VM is not running.
)

if exist "%VM_PATH%" (
    echo Deleting "%VM_PATH%"...
    vagrant destroy -f %VM_NAME%
    rmdir /s /q "%VM_PATH%"
    echo Deleted successfully. : "%VM_NAME%"
) else (
    echo Directory does not exist: "%VM_PATH%"
)

if exist "%VDI_FILE%" (
    %VBOXMANAGE% closemedium disk "%VDI_FILE%"
    del /f /q "%VDI_FILE%"
    echo %VDI_FILE% has been deleted.
) else (
    echo %VDI_FILE% does not exist.
)

4) vagrant 에러

DHCP 에러

이 오류는 VirtualBox에서 중복된 호스트 전용 네트워크(Host-Only Network) DHCP 설정이 충돌하여 발생하는 문제입니다.

VirtualBox에서 중복된 DHCP 설정을 비활성화하거나 삭제합니다.

Vagrant 1.1편-Vagrant설치 를 참고하세요.

https://javata.tistory.com/59

 

Vagrant 1.1편-Vagrant설치

가상환경을 구축하는 방법으로 Virtual Box, VMWare 등을 사용할 수 있습니다.Vagrant 를 사용하면 스크립트로 원하는  VM 을 쉽게 구축할 수 있습니다.또 여러개의  VM 도 쉽게 만들 수 있습니다.코딩을

javata.tistory.com

 

 

Oracle XE 다운로드 에러

Oracle XE 설치 파일은 2GB 정도 됩니다.

그래서 네크워크가 느린 환경에서는 다운로드 에러가 발생하기도 합니다.

이 경우 Oracle XE 를 직접 다운로드하여 winscp 나 ftp 같은 프로그램으로 VM 에 넣어야 할 수도 있습니다.

다음 URL 에 Oracle XE 를 다운로드 합니다.

https://www.oracle.com/database/technologies/xe-downloads.html

 

Oracle Database Express Edition (XE) Downloads

Support Oracle Database Express Edition (XE) is a community supported edition of the Oracle Database family. Please go to the Oracle Database XE Community Support Forum for help, feedback, and enhancement requests. Note: Oracle Support Services only provid

www.oracle.com

 

Rocky Linux 9 의 경우 OL8 을 다운로드합니다.

OL7 vs OL8 선택 기준


OS 다운로드할 버전
Rocky Linux 9, Oracle Linux 8, RHEL 8, CentOS 8 OL8 (올바른 선택)
Rocky Linux 8, Oracle Linux 7, RHEL 7, CentOS 7 OL7 (구버전)

5) SQL Developer 설치

정성적으로 Oracle 에 연결이 가능한지 테스트하기 위해서는 Oracle 에 연결가능한 tool 로 테스트해야 합니다.

Oracle SQLDevloper 로 테스트해보겠습니다.

다음 URL 에서 다운받아서 압축을 풀고 실행하면 됩니다.

https://www.oracle.com/database/sqldeveloper/technologies/download/

 

Oracle SQL Developer Downloads

This archive. will work on a 32 or 64 bit Windows OS. The bit level of the JDK you install will determine if it runs as a 32 or 64 bit application. This download does not include the required Oracle Java JDK. You will need to install it if it's not already

www.oracle.com

 

저는 아래 버전을 다운받았습니다.

6) Oracle 연결 테스트

SQL Developer 에서 새 접속을 클릭 후에 다음과 같이 입력합니다.

테스트를 클릭하여 상태에 성공이 나오면 됩니다.

'유틸리티 > Vagrant' 카테고리의 다른 글

Vagrant 1.3편-Rocky Linux 에 MariaDB 설치  (1) 2025.04.05
Vagrant 1.1편-Vagrant설치  (1) 2025.03.30
Posted by 제이브레인
,

가상환경을 구축하는 방법으로 Virtual Box, VMWare 등을 사용할 수 있습니다.

Vagrant 를 사용하면 스크립트로 원하는  VM 을 쉽게 구축할 수 있습니다.

또 여러개의  VM 도 쉽게 만들 수 있습니다.

코딩을 하다보면 Oracle, Mysql 등의 Database 를 설치하거나 redis 등의 서버를 설치하는 경우가 있습니다.

docker 를 사용할 수도 있지만 ssh 로 직접 설정을 바꿔보고 싶은 경우 VM 이 좀더 나은 선택일 수도 있습니다.

1) Virtual Box 설치

VirtualBox는 오라클에서 개발한 가상화 소프트웨어입니다.

설치 방법은 아래 URL  에서 자신의 OS 에 맞게 다운로드하면 됩니다.

https://www.virtualbox.org/wiki/Downloads

 

Downloads – Oracle VirtualBox

This VirtualBox Extension Pack Personal Use and Educational License governs your access to and use of the VirtualBox Extension Pack. It does not apply to the VirtualBox base package and/or its source code, which are licensed under version 3 of the GNU Gene

www.virtualbox.org

저는 Windows 에 설치해보겠습니다.

 

 

기본설치로 진행해보겠습니다.

Next 를 계속 눌러서 설치를 진행하면 됩니다.

 

설치는 시간이 걸립니다. 느긋하게 기다리면 됩니다.

 

확장 패키지를 다운로드합니다.

이제 확장 패키지를 설치합니다.

도구 > 확장 패키지 관리자를 선택합니다.

 

설치를 클릭합니다.

 

다운받은 확장 패키지를 선택 후 설치합니다.

 

설치가 완료되면 확장 패키지가 활성화 됩니다.

 

2) Vagrant 설치

아래 URL 에서 OS 맞게 선택 후 설치합니다.

https://developer.hashicorp.com/vagrant/install

 

Install | Vagrant | HashiCorp Developer

Explore Vagrant product documentation, tutorials, and examples.

developer.hashicorp.com

 

 Windows 64 비트의경우 AMD64 를 선택하면 됩니다.

 

License 동의 후에 Install 을 클릭합니다.

 

설치에 시간이 걸리므로 잠시 기다려주면 됩니다.

 

 

3) NAT 네크워크 설정

Vagrant 로 VM 생성 시 NAT 네크워크 설정이 필요합니다.

 

호스트 전용 네트워크를 클릭합니다.

 

NAT 네트워크는 DHCP 를 사용하거나 직접 IP 를 설정할 수  있습니다.

DHCP 설정의 경우 Client 에서 IP 설정을 할 필요가 없지만 할당된 IP가 변경이 될 수 있으므로 DHCP 를 사용하지 않겠습니다.

DHCP 를 사용해도 됩니다. 본인이 선택을 하면 됩니다.

우선 DHCP 비활성화로 예를 들어 설명하겠습니다.

아래와 같이 DHCP 서버 활성화를 체크해제하여 비활성화 시킵니다.

 

어댑터 탭을 보면 수동으로 어댑터 설정이 되어 있습니다.

IPv4 주소는 192.168.56.1 이고 서브넷 마스크는 255.255.255.0  입니다.

Client IP 는 192.168.56.xx 로 설정이 가능합니다.

만약 다른 IPv4 주소를 사용하고 싶으면 다른 주소를 선택하면 됩니다.

'유틸리티 > Vagrant' 카테고리의 다른 글

Vagrant 1.3편-Rocky Linux 에 MariaDB 설치  (1) 2025.04.05
Vagrant 1.2편-Rocky Linux 에 Oracle 설치  (2) 2025.03.31
Posted by 제이브레인
,

python 으로 Database 라이브러리는 다양한 종류가 존재합니다.

다양한 Python 데이터베이스 검색 라이브러리를 비교한 표를 정리하면 다음과 같습니다.

라이브러리 지원 데이터베이스 주요 특징 장점 단점
SQLAlchemy MySQL, PostgreSQL, SQLite, Oracle, MSSQL 등 강력한 ORM(Object Relational Mapping) 기능 제공 객체 지향적인 방식으로 SQL 조작 가능, 다양한 DB 지원 학습 곡선이 가파를 수 있음
Peewee MySQL, PostgreSQL, SQLite 등 경량 ORM, 간결한 코드 사용법이 간단하고 빠름 대규모 프로젝트에는 부족할 수 있음
Django ORM PostgreSQL, MySQL, SQLite, Oracle Django 프레임워크와 통합된 ORM Django 사용 시 강력한 기능 제공 Django 프로젝트 외 사용이 번거로울 수 있음
PonyORM MySQL, PostgreSQL, SQLite 등 Pythonic한 쿼리 지원 직관적인 SQL 문법, 자동화된 관계 관리 대규모 시스템에서는 성능 문제 가능성 있음
Tortoise-ORM PostgreSQL, MySQL, SQLite 등 비동기 ORM 지원 asyncio 기반, 빠른 비동기 쿼리 처리 비동기 환경이 아닌 경우 사용이 어렵거나 필요 없음
TinyDB JSON 기반의 파일 DB NoSQL 방식, 경량 데이터베이스 설정이 간단하고 파일 기반 저장 가능 대용량 데이터 처리에 적합하지 않음
Whoosh 자체 인덱싱 엔진 사용 풀텍스트 검색 엔진 Python 내에서 검색 엔진 구축 가능 대규모 검색 시스템에는 부족할 수 있음
Elasticsearch-py Elasticsearch 강력한 검색 엔진과 연동 대량 데이터 검색 최적화 설정 및 유지보수가 어려울 수 있음
PyMongo MongoDB NoSQL 데이터베이스 연결 라이브러리 JSON 기반 문서 저장, 유연한 데이터 구조 관계형 데이터베이스 지원 부족
Qdrant Client Qdrant (벡터 데이터베이스) 벡터 기반 검색 지원 AI 및 유사 이미지 검색 등에 최적화 전통적인 SQL 검색이 아닌 경우 부적합

어떤 데이터베이스를 사용하려는지에 따라 적절한 라이브러리를 선택하면 됩니다! 😊

SQLAlchemy 를 사용해서 Database 를 검색하는 기능을 추가해보겠습니다.

간단한 Member 라는 테이블을 만들어서 Name 으로 검색하고 결과를 출력하는 기능을 추가하겠습니다.

1) 코드 생성

Database 는 간단하게 파일로 생성할 수 있는 SQLite 를 사용하였습니다.

ChatGPT 에 사용한 프롬프트입니다.

pyqt5 로 실행 시 window 에 다음을 추가해주세요.
첫째 라인 : Meber Name 라는 GroupBox 로 둘러싸인 LineEdit, Search 버튼
둘째 라인 : Name, Age 이라는 Column 을 가지는 Grid

초기 실행 시 SQLite members.db 를 존재하지 않으면 생성하고 Member 테이블을 생성한 후에 테스트 데이터를 추가합니다.
테스트 데이터에는 Name 이 값은 Member 도 여러명 존재하고 Name 이 고유한 데이터도 존재해야 합니다.

SQLine 에 String Name, Intger Age 라는 Member 테이블을 만듭니다.
Seach 버튼을 클릭하면 SqlAlchemy 로 Member 테이블을 검색하여 Member Name LineEdit 에 입력된 값으로 검색하여 Name 이 일치하는 
레코드를 찾습니다.
일치하는 데이터를 찾으면 Grid 에 Name, Age 를 추가합니다.

 

생성된 코드는 다음과 같습니다.

위 코드는 아래 git 에서도 다운받을 수 있습니다.

git clone git@github.com:jbpark/jbDeskExample.git
cd jbDeskExample/jbDesk/ch2.1

 

search_database.py

from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QGroupBox, QLineEdit, QPushButton, QTableWidget, \
    QTableWidgetItem, QHBoxLayout
from sqlalchemy import create_engine, Column, Integer, String, select
from sqlalchemy.orm import declarative_base, sessionmaker
import os

# 데이터베이스 설정
DATABASE_URL = "sqlite:///members.db"
engine = create_engine(DATABASE_URL, echo=True)
Base = declarative_base()
Session = sessionmaker(bind=engine)
session = Session()


# Member 테이블 정의
class Member(Base):
    __tablename__ = "members"
    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False)
    age = Column(Integer, nullable=False)


# 데이터베이스 및 테이블 생성 (존재하지 않으면 생성)
if not os.path.exists("members.db"):
    Base.metadata.create_all(engine)


    # 테스트 데이터 추가
    def add_test_data():
        test_members = [
            Member(name="Alice", age=30),
            Member(name="Alice", age=28),
            Member(name="Bob", age=25),
            Member(name="Charlie", age=35),
            Member(name="David", age=40)
        ]
        session.add_all(test_members)
        session.commit()


    add_test_data()


class MemberSearchApp(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        main_layout = QVBoxLayout()

        # 첫째 라인: Member Name GroupBox + Search 버튼
        name_layout = QHBoxLayout()
        self.name_group = QGroupBox("Member Name")
        self.name_input = QLineEdit()
        group_layout = QVBoxLayout()
        group_layout.addWidget(self.name_input)
        self.name_group.setLayout(group_layout)

        self.search_button = QPushButton("Search")
        self.search_button.clicked.connect(self.search_member)

        name_layout.addWidget(self.name_group)
        name_layout.addWidget(self.search_button)
        main_layout.addLayout(name_layout)

        # 둘째 라인: Grid Table (Name, Age)
        self.table = QTableWidget()
        self.table.setColumnCount(2)
        self.table.setHorizontalHeaderLabels(["Name", "Age"])
        main_layout.addWidget(self.table)

        self.setLayout(main_layout)

        self.setWindowTitle("Member Search")
        self.setGeometry(100, 100, 400, 300)

    def search_member(self):
        name = self.name_input.text().strip()
        if not name:
            return

        # SQLAlchemy 검색
        stmt = select(Member).where(Member.name == name)
        results = session.execute(stmt).scalars().all()

        self.table.setRowCount(len(results))
        for index, member in enumerate(results):
            self.table.setItem(index, 0, QTableWidgetItem(member.name))
            self.table.setItem(index, 1, QTableWidgetItem(str(member.age)))


if __name__ == "__main__":
    app = QApplication([])
    window = MemberSearchApp()
    window.show()
    app.exec_()

이제 UI가 다음과 같이 변경되었습니다:

  1. 첫째 라인
    • "Member Name"을 포함하는 QGroupBox 내에 QLineEdit
    • Search 버튼
  2. 둘째 라인
    • QTableWidget을 사용해 "Name", "Age" 컬럼을 포함하는 그리드
  3. 기능 추가
    • SQLite 데이터베이스 members.db를 생성하고 Member 테이블을 설정
    • 테스트 데이터 추가 (동일한 Name을 가진 여러 레코드 포함)
    • Search 버튼 클릭 시 Name으로 검색하여 그리드에 일치하는 레코드 표시

2) 코드 실행

SQLAlchemy 를 사용하므로 패키지를 실행전에 패키지를 설치해야 합니다.

pip install sqlalchemy

생성된 테스트 데이터를 보면 Alice 로 Name 이 동일한 Member 가 여러명 있는 것을 확인할 수 있습니다.

검색 시 아래와 같이 모든 Member 가 Grid 에 표시됨을 확인 할 수 있습니다.

코딩이 정말 쉽지 않나요?

아라비안 나이트에 알라딘의 요술램프가 있다면 요즘은 ChatGPT 같은 인공 지능 요술램프가 있는 것 같습니다.

 

3) JbDesk 코드 통합

JbDesk 에 통합 후 실행한 이미지입니다.

통합된 코드는 다음과 같이 git 에서 다운로드 할 수 있습니다.

git clone git@github.com:jbpark/jbDeskExample.git
cat jbDeskExample/jbDesk/ch2.1/jbdesk.py

 

코드 실행은 다음과 같이 run.bat 파일로 실행이 가능합니다.

run.bat 에는 패키지 설치도 포함되어 있습니다.

@echo off
set CONDA_ENV_NAME=jbdesk38_64

:: Conda 활성화
call conda activate base

:: Conda 환경 목록 확인 후 존재하지 않으면 생성
conda info --envs | findstr /C:"%CONDA_ENV_NAME%" >nul
if %errorlevel% neq 0 (
    echo Creating conda environment: %CONDA_ENV_NAME%
    conda create -y -n %CONDA_ENV_NAME% python=3.8
)

:: Conda 환경 활성화
call conda activate %CONDA_ENV_NAME%

:: PyQt5 설치
pip show PyQt5 >nul 2>nul
if %errorlevel% neq 0 (
    echo Installing PyQt5...
    pip install PyQt5
)

:: pytz 설치
pip show pytz >nul 2>nul
if %errorlevel% neq 0 (
    echo Installing pytz...
    pip install pytz
)

:: sqlalchemy 설치
pip show sqlalchemy >nul 2>nul
if %errorlevel% neq 0 (
    echo Installing sqlalchemy...
    pip install sqlalchemy
)

:: jbdesk.py 실행
python jbdesk.py

Posted by 제이브레인
,

Python에서는 메서드, 함수, 변수, 파일 이름은 snake_case, 클래스 이름은 CamelCase ,
**상수는 ALL_CAPS**로 작성하는 것이 표준입니다. 🚀

정리 요약

항목 표기법 표기법
클래스(Class) CamelCase class StockManager:
메서드(Method) snake_case def process_order(self):
함수(Function) snake_case def calculate_profit():
변수(Variable) snake_case stock_price = 1000
상수(Constant) ALL_CAPS MAX_RETRIES = 3
파일(File) snake_case.py order_processor.py
모듈(Module) snake_case trading_utils.py
패키지(Package) snake_case (소문자, 밑줄 없이) finance

 

1) 코드 정리

py 파일에서 많이 사용하는 표기법으로 jbdesk.py 파일을 정리하고 method 의 경우 string_util, log_util 로 분리하였습니다.

PyCharm 에서는 리팩토링 기능을 제공합니다.

그래서 다른 파일로 이동하거나 method 로 분리할 때 쉽게 할 수 있습니다.

string 관련 유틸리티 method 를 string_util.py 로 분리해보겠습니다.

우선 lib/string_util.py 를 생성합니다.

그리고 jbdesk.py 에서 다음과 같이  to_pascal_case_line 과 같은 string 관련 유틸리티 method 를 마우스로 선택 후에 

리팩터링 > 이동을 선택합니다.

 

대상 위치를 string_util.py 를 선택 후 [리팩토링] 버튼을 클릭합니다.

 

그러면 해당 method 들이 string_util 로 이동이 됩니다.

특정 라인을 method 로 추출하기 위해서는 아래와 같이 특정 라인을 선택 후 리팩터링 > 메서드 추출을 선택합니다.

 

2) 리팩터링 코드

리팩터링한 코드는 아래 위치에서 확인이 가능합니다.

git clone git@github.com:jbpark/jbDeskExample.git
cd jbDeskExample/jbDesk/ch1.6
Posted by 제이브레인
,