convert to gitea
This commit is contained in:
12
apps/rust_gyber/.env
Normal file
12
apps/rust_gyber/.env
Normal file
@ -0,0 +1,12 @@
|
||||
# .env
|
||||
|
||||
# 암호화 키 (Base64 인코딩된 16바이트 키)
|
||||
DB_ENCRYPTION_KEY="YWN0aW9uITEyM3NxdWFyZQ=="
|
||||
|
||||
# 데이터베이스 접속 정보
|
||||
DB_HOST="localhost"
|
||||
DB_NAME="gyber"
|
||||
DB_PORT="3306"
|
||||
|
||||
# 로그 레벨 (선택 사항)
|
||||
RUST_LOG="info"
|
||||
2696
apps/rust_gyber/Cargo.lock
generated
Normal file
2696
apps/rust_gyber/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
apps/rust_gyber/Cargo.toml
Normal file
36
apps/rust_gyber/Cargo.toml
Normal file
@ -0,0 +1,36 @@
|
||||
[package]
|
||||
name = "gyber"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Collects hardware information and updates MariaDB"
|
||||
|
||||
[dependencies]
|
||||
# --- Serde 수정: "derive"와 "deserialize" 기능 모두 명시 ---
|
||||
serde = { version = "1.0.204", features = ["derive"] } # 최신 버전 확인 및 기능 수정
|
||||
serde_json = "1.0.120"
|
||||
|
||||
# MySQL/MariaDB Async Driver
|
||||
mysql_async = "0.34.1"
|
||||
|
||||
# Tokio: 비동기 런타임
|
||||
tokio = { version = "1.39.1", features = ["full"] }
|
||||
|
||||
# Logging (log4rs 사용)
|
||||
log = "0.4.22"
|
||||
log4rs = "1.3.0"
|
||||
serde_yaml = "0.9" # log4rs YAML 설정용
|
||||
|
||||
# Anyhow: 에러 처리
|
||||
anyhow = "1.0.86"
|
||||
|
||||
# Chrono: 날짜/시간 처리
|
||||
chrono = "0.4.38"
|
||||
|
||||
# Dotenvy: .env 파일 로드
|
||||
dotenvy = "0.15.7"
|
||||
|
||||
# Base64: 키 인코딩/디코딩
|
||||
base64 = "0.22.1"
|
||||
|
||||
# AES-GCM: 복호화
|
||||
aes-gcm = { version = "0.10.3", features = ["alloc"] }
|
||||
4
apps/rust_gyber/config/config.json
Normal file
4
apps/rust_gyber/config/config.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"json_file_path": "/pcinfo",
|
||||
"db_config_path": "config/db.enc"
|
||||
}
|
||||
1
apps/rust_gyber/config/db.enc
Normal file
1
apps/rust_gyber/config/db.enc
Normal file
@ -0,0 +1 @@
|
||||
<EFBFBD><EFBFBD><EFBFBD>?a&<26><>@<03>̯<EFBFBD><CCAF><1B>#<23>1,9<>K<EFBFBD>j=<3D>2Oͧ?Ű昩<C5B0>H<EFBFBD><48><EFBFBD><EFBFBD><EFBFBD><EFBFBD>%<25><>KQ(<28><><EFBFBD>5<><1D>ʰl<CAB0><6C>䶫583
|
||||
48
apps/rust_gyber/config/log4rs.yaml
Normal file
48
apps/rust_gyber/config/log4rs.yaml
Normal file
@ -0,0 +1,48 @@
|
||||
# config/log4rs.yaml
|
||||
|
||||
appenders:
|
||||
console:
|
||||
kind: console
|
||||
target: stdout
|
||||
encoder:
|
||||
# 파일명({f})과 라인번호({L}) 추가
|
||||
pattern: "{d(%Y-%m-%d %H:%M:%S)} [{h({l:<5})}] {({t})} [{f}:{L}] - {m}{n}"
|
||||
|
||||
info_file:
|
||||
kind: file
|
||||
path: "logs/info.log"
|
||||
append: true
|
||||
encoder:
|
||||
# 파일명({f})과 라인번호({L}) 추가
|
||||
pattern: "{d(%Y-%m-%d %H:%M:%S)} [{l:<5}] {({t})} [{f}:{L}] - {m}{n}"
|
||||
filters:
|
||||
- kind: threshold
|
||||
level: info
|
||||
|
||||
error_file:
|
||||
kind: file
|
||||
path: "logs/error.log"
|
||||
append: true
|
||||
encoder:
|
||||
# 파일명({f})과 라인번호({L}) 추가 (기존에도 있었음)
|
||||
pattern: "{d(%Y-%m-%d %H:%M:%S)} [{l:<5}] {({t})} [{f}:{L}] - {m}{n}"
|
||||
filters:
|
||||
- kind: threshold
|
||||
level: error
|
||||
|
||||
root:
|
||||
level: info # 또는 debug 등 필요 레벨
|
||||
appenders:
|
||||
- console
|
||||
- info_file
|
||||
- error_file
|
||||
|
||||
# 특정 모듈에 다른 로깅 레벨 적용 가능 (선택 사항)
|
||||
# loggers:
|
||||
# gyber::db: # 예시: gyber::db 모듈은 DEBUG 레벨까지 출력
|
||||
# level: debug
|
||||
# appenders:
|
||||
# - console
|
||||
# - info_file
|
||||
# - error_file
|
||||
# additivity: false # true면 root 설정도 상속받음, false면 이 설정만 적용
|
||||
51
apps/rust_gyber/src/config_reader.rs
Normal file
51
apps/rust_gyber/src/config_reader.rs
Normal file
@ -0,0 +1,51 @@
|
||||
// src/config_reader.rs
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize; // JSON 역직렬화를 위해 필요
|
||||
use std::fs; // 파일 시스템 접근 (파일 읽기)
|
||||
use std::path::Path; // 파일 경로 관련 작업
|
||||
|
||||
// config.json 파일 구조에 맞는 설정 구조체 정의
|
||||
// Debug 트레잇은 println! 등에서 구조체를 보기 좋게 출력하기 위해 추가
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AppConfig {
|
||||
// JSON 파일의 "json_file_path" 키와 필드매핑
|
||||
// serde(rename = "...") 어노테이션은 JSON 키 이름과 Rust 필드 이름이 다를 때 사용!!
|
||||
#[serde(rename = "json_file_path")]
|
||||
pub json_files_path: String, // 처리할 JSON 파일들이 있는 디렉토리 경로
|
||||
|
||||
// JSON 파일의 "db_config_path" 키와 이 필드를 매핑
|
||||
// config.json 파일에 이 키가 존재해야 함!!
|
||||
#[serde(rename = "db_config_path")]
|
||||
pub db_config_path: String, // 암호화된 DB 설정 파일 경로
|
||||
}
|
||||
|
||||
// 설정 파일을 읽어 AppConfig 구조체로 반환하는 함수
|
||||
pub fn read_app_config(config_path: &str) -> Result<AppConfig> {
|
||||
// 입력받은 설정 파일 경로 문자열을 Path 객체로 변환
|
||||
let path = Path::new(config_path);
|
||||
|
||||
// 설정 파일이 실제로 존재하는지 확인
|
||||
if !path.exists() {
|
||||
// 파일이 없으면 anyhow::bail! 매크로를 사용하여 에러를 생성하고 즉시 반환
|
||||
anyhow::bail!("설정 파일을 찾을 수 없습니다: {}", config_path);
|
||||
}
|
||||
|
||||
// 파일 내용을 문자열로 읽기
|
||||
// fs::read_to_string은 Result를 반환하므로 '?' 연산자로 에러 처리
|
||||
// .with_context()는 에러 발생 시 추가적인 문맥 정보를 제공 (anyhow 기능)
|
||||
let config_content = fs::read_to_string(path)
|
||||
.with_context(|| format!("설정 파일 읽기 실패: {}", config_path))?;
|
||||
|
||||
// 읽어온 JSON 문자열을 AppConfig 구조체로 파싱(역직렬화)
|
||||
// serde_json::from_str은 Result를 반환하므로 '?' 연산자로 에러 처리
|
||||
// .with_context()로 파싱 실패 시 에러 문맥 추가
|
||||
let app_config: AppConfig = serde_json::from_str(&config_content)
|
||||
.with_context(|| format!("설정 파일 JSON 파싱 실패: {}", config_path))?;
|
||||
|
||||
// 성공적으로 읽고 파싱한 경우, 디버그 레벨 로그로 설정값 출력
|
||||
log::debug!("설정 파일 로드 완료: {:?}", app_config);
|
||||
|
||||
// 파싱된 AppConfig 구조체를 Ok() 로 감싸서 반환
|
||||
Ok(app_config)
|
||||
}
|
||||
143
apps/rust_gyber/src/db/compare.rs
Normal file
143
apps/rust_gyber/src/db/compare.rs
Normal file
@ -0,0 +1,143 @@
|
||||
use super::connection::DbPool;
|
||||
use crate::file::json_reader::ProcessedHwInfo; // 처리된 JSON 데이터 구조체
|
||||
use anyhow::{Context, Result};
|
||||
use log::{debug, error, info, warn};
|
||||
use mysql_async::prelude::*;
|
||||
use mysql_async::{params, FromRowError, Row};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
// DB에서 가져온 자원 정보를 담는 구조체
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct DbResource {
|
||||
pub resource_id: i64, // 자원 고유 ID
|
||||
pub resource_name: String, // 자원 이름 (모델명 등)
|
||||
pub serial_num: String, // <-- DB에 저장된 '합성 키'
|
||||
}
|
||||
|
||||
// MySQL Row 에서 DbResource 로 변환하는 구현
|
||||
impl TryFrom<Row> for DbResource {
|
||||
type Error = FromRowError;
|
||||
fn try_from(mut row: Row) -> std::result::Result<Self, Self::Error> {
|
||||
Ok(DbResource {
|
||||
resource_id: row.take("resource_id").ok_or_else(|| FromRowError(row.clone()))?,
|
||||
resource_name: row.take("resource_name").ok_or_else(|| FromRowError(row.clone()))?,
|
||||
// DB 프로시저가 반환하는 serial_num 컬럼 (합성 키)
|
||||
serial_num: row.take("serial_num").ok_or_else(|| FromRowError(row.clone()))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 데이터 비교 결과를 담는 구조체
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ComparisonResult {
|
||||
pub adds: Vec<ProcessedHwInfo>, // DB에 추가(또는 할당)해야 할 항목들
|
||||
pub deletes: Vec<DbResource>, // DB에서 할당 해제해야 할 항목들
|
||||
}
|
||||
|
||||
// 특정 사용자의 DB에 등록된 자원 목록(합성 키 포함)을 조회하는 함수
|
||||
async fn get_existing_resources(
|
||||
pool: &DbPool,
|
||||
account_name: &str, // 사용자 계정 이름 (JSON의 hostname과 매핑)
|
||||
) -> Result<Vec<DbResource>> {
|
||||
debug!("'{}' 계정의 기존 자원 조회 (sp_get_resources_by_account)...", account_name);
|
||||
let mut conn = pool.get_conn().await.context("DB 커넥션 얻기 실패")?;
|
||||
|
||||
// 사용자 계정명으로 자원을 조회하는 저장 프로시저 호출
|
||||
let query = r"CALL sp_get_resources_by_account(:p_account_name)";
|
||||
let params = params! { "p_account_name" => account_name };
|
||||
|
||||
// 프로시저 실행 및 결과 매핑
|
||||
let results: Vec<std::result::Result<DbResource, FromRowError>> = conn.exec_map(
|
||||
query, params, |row: Row| DbResource::try_from(row)
|
||||
).await.with_context(|| format!("sp_get_resources_by_account 프로시저 실패: 계정='{}'", account_name))?;
|
||||
|
||||
// 결과 처리 및 유효성 검사
|
||||
let total_rows = results.len();
|
||||
let mut db_resources = Vec::with_capacity(total_rows);
|
||||
let mut error_count = 0;
|
||||
for result in results {
|
||||
match result {
|
||||
Ok(resource) if !resource.serial_num.is_empty() => {
|
||||
// 유효한 합성 키를 가진 자원만 리스트에 추가
|
||||
db_resources.push(resource);
|
||||
}
|
||||
Ok(resource) => {
|
||||
// DB에 빈 합성 키가 저장된 경우 경고
|
||||
warn!("DB에서 유효하지 않은 합성 키 가진 자원 발견 (비교 제외): ID={}, Key='{}'", resource.resource_id, resource.serial_num);
|
||||
error_count += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
// Row 변환 오류 처리
|
||||
error!("get_existing_resources: DB Row 변환 에러 (Row 무시됨): {:?}", e);
|
||||
error_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let success_count = db_resources.len();
|
||||
debug!("'{}' 계정 기존 자원 {}개 조회 완료 (유효 키/변환 성공: {}개, 실패/무효: {}개).", account_name, total_rows, success_count, error_count);
|
||||
|
||||
Ok(db_resources)
|
||||
}
|
||||
|
||||
|
||||
// JSON 데이터와 DB 데이터를 비교하여 추가/삭제 대상을 결정하는 함수
|
||||
pub async fn compare_data(
|
||||
pool: &DbPool,
|
||||
processed_data: &HashMap<String, Vec<ProcessedHwInfo>>, // 호스트명별로 그룹화된 JSON 데이터
|
||||
) -> Result<HashMap<String, ComparisonResult>> {
|
||||
info!("JSON 데이터와 DB 데이터 비교 시작 (ADD/DELETE)...");
|
||||
let mut all_changes: HashMap<String, ComparisonResult> = HashMap::new();
|
||||
|
||||
// 각 호스트(사용자)별로 데이터 비교 수행
|
||||
for (hostname, json_items) in processed_data {
|
||||
info!("'{}' 호스트 데이터 비교 중...", hostname);
|
||||
// 해당 호스트(사용자)의 DB 자원 목록 조회
|
||||
let db_items = match get_existing_resources(pool, hostname).await {
|
||||
Ok(items) => items,
|
||||
Err(e) => {
|
||||
// DB 조회 실패 시 해당 호스트 건너뛰기
|
||||
error!("'{}' DB 자원 조회 실패: {}. 건너<0xEB><0x9B><0x81>니다.", hostname, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// --- 비교를 위한 자료구조 생성 (합성 키 기준) ---
|
||||
// JSON 데이터 Map (Key: 합성 키 serial_key)
|
||||
let json_map: HashMap<&String, &ProcessedHwInfo> = json_items.iter()
|
||||
.filter(|item| !item.serial_key.is_empty()) // 빈 키 제외
|
||||
.map(|item| (&item.serial_key, item))
|
||||
.collect();
|
||||
|
||||
// DB 데이터 Map (Key: 합성 키 serial_num)
|
||||
let db_map: HashMap<&String, &DbResource> = db_items.iter()
|
||||
// get_existing_resources 에서 이미 빈 키 필터링됨
|
||||
.map(|item| (&item.serial_num, item))
|
||||
.collect();
|
||||
|
||||
// 각 데이터 소스의 합성 키 Set 생성
|
||||
let json_keys: HashSet<&String> = json_map.keys().cloned().collect();
|
||||
let db_keys: HashSet<&String> = db_map.keys().cloned().collect();
|
||||
// --- 비교 자료구조 생성 끝 ---
|
||||
|
||||
// Adds 찾기: JSON 에는 있으나 DB 에는 없는 합성 키
|
||||
let adds: Vec<ProcessedHwInfo> = json_keys.difference(&db_keys)
|
||||
.filter_map(|key| json_map.get(key).map(|&item| item.clone()))
|
||||
.collect();
|
||||
|
||||
// Deletes 찾기: DB 에는 있으나 JSON 에는 없는 합성 키
|
||||
let deletes: Vec<DbResource> = db_keys.difference(&json_keys)
|
||||
.filter_map(|key| db_map.get(key).map(|&item| item.clone()))
|
||||
.collect();
|
||||
|
||||
// 변경 사항 결과 저장
|
||||
if !adds.is_empty() || !deletes.is_empty() {
|
||||
debug!("'{}': 추가 {}개, 삭제 {}개 발견.", hostname, adds.len(), deletes.len());
|
||||
all_changes.insert(hostname.clone(), ComparisonResult { adds, deletes });
|
||||
} else {
|
||||
debug!("'{}': 변경 사항 없음.", hostname);
|
||||
}
|
||||
}
|
||||
|
||||
info!("데이터 비교 완료.");
|
||||
Ok(all_changes)
|
||||
}
|
||||
37
apps/rust_gyber/src/db/connection.rs
Normal file
37
apps/rust_gyber/src/db/connection.rs
Normal file
@ -0,0 +1,37 @@
|
||||
// src/db/connection.rs
|
||||
|
||||
use crate::file::decrypt::DbCredentials; // DB 인증 정보 구조체 가져오기
|
||||
use anyhow::{Context, Result};
|
||||
use mysql_async::{Opts, OptsBuilder, Pool}; // mysql_async 관련 타입 가져오기
|
||||
|
||||
// 편의를 위한 타입 별칭 (Type Alias)
|
||||
pub type DbPool = Pool;
|
||||
|
||||
// DB 인증 정보를 사용하여 데이터베이스 커넥션 풀을 생성하는 비동기 함수
|
||||
pub async fn connect_db(creds: &DbCredentials) -> Result<DbPool> {
|
||||
let opts_builder = OptsBuilder::default()
|
||||
.ip_or_hostname(creds.host.clone()) // ip_addr 대신 ip_or_hostname 사용, 소유권 문제로 clone
|
||||
.tcp_port(creds.port) // tcp_port 사용
|
||||
.user(Some(creds.user.clone())) // user 사용, 소유권 문제로 clone
|
||||
.pass(Some(creds.pass.clone())) // pass 사용, 소유권 문제로 clone
|
||||
.db_name(Some(creds.db.clone())) // db_name 사용, 소유권 문제로 clone
|
||||
.prefer_socket(false);
|
||||
|
||||
// OptsBuilder에서 Opts 생성
|
||||
let opts: Opts = opts_builder.into(); // .into()를 사용하여 Opts로 변환
|
||||
|
||||
let pool = Pool::new(opts); // Pool::new는 Opts를 인자로 받음
|
||||
|
||||
match pool.get_conn().await {
|
||||
Ok(_conn) => {
|
||||
log::info!("데이터베이스 연결 테스트 성공: {}", creds.db);
|
||||
Ok(pool)
|
||||
}
|
||||
Err(e) => {
|
||||
Err(e).context(format!(
|
||||
"데이터베이스({}) 연결 실패: 호스트={}, 사용자={}",
|
||||
creds.db, creds.host, creds.user
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
9
apps/rust_gyber/src/db/mod.rs
Normal file
9
apps/rust_gyber/src/db/mod.rs
Normal file
@ -0,0 +1,9 @@
|
||||
// src/db/mod.rs
|
||||
pub mod connection;
|
||||
pub mod compare;
|
||||
pub mod sync;
|
||||
|
||||
// main.rs에서 직접 경로를 사용하므로 pub use 제거
|
||||
// pub use connection::connect_db;
|
||||
// pub use compare::{compare_data, ComparisonResult};
|
||||
// pub use delsert::execute_delsert;
|
||||
208
apps/rust_gyber/src/db/sync.rs
Normal file
208
apps/rust_gyber/src/db/sync.rs
Normal file
@ -0,0 +1,208 @@
|
||||
use super::connection::DbPool;
|
||||
use super::compare::DbResource; // 할당 해제 시 DB 정보 사용
|
||||
use crate::file::json_reader::ProcessedHwInfo; // 추가/할당 시 JSON 정보 사용
|
||||
use anyhow::{Context, Result};
|
||||
use log::{debug, error, info, warn};
|
||||
use mysql_async::prelude::*;
|
||||
use mysql_async::{params, Conn, Row, Value};
|
||||
|
||||
// ============================================================
|
||||
// 내부 유틸리티 함수: sp_sync_resource_info_from_scan 프로시저 호출
|
||||
// ============================================================
|
||||
async fn call_sync_procedure(
|
||||
conn: &mut Conn,
|
||||
admin_user_id: Option<i32>, // p_admin_user_id (Rust에서는 보통 None)
|
||||
actor_description: &str, // p_actor_description (작업 주체 설명, 예: "RustFileSync-hostname")
|
||||
user_account_name: &str, // p_user_account_name (사용자 계정명, hostname)
|
||||
category: &str, // p_category (자산 카테고리 이름)
|
||||
manufacturer: &str, // p_manufacturer (제조사)
|
||||
resource_name: &str, // p_resource_name (모델명)
|
||||
composite_key: &str, // p_serial_num (DB 프로시저의 파라미터명은 p_serial_num 이지만, 값은 '합성 키')
|
||||
spec_value: &str, // p_spec_value (사양 값, 문자열)
|
||||
spec_unit: &str, // p_spec_unit (사양 단위, 문자열)
|
||||
detected_by: &str, // p_detected_by (스캔 정보 출처 등, hostname 사용)
|
||||
change_type: u8, // p_change_type (1: Add/Assign, 2: Unassign)
|
||||
) -> Result<String> // 프로시저 결과 메시지 반환
|
||||
{
|
||||
let action = match change_type { 1 => "추가/할당", 2 => "할당 해제", _ => "알수없음" };
|
||||
// 로그: 작업 시작 알림 (합성 키 포함)
|
||||
debug!("'{}'에 의한 '{}' 자원 동기화 ({}) 시작: Key='{}'",
|
||||
actor_description, user_account_name, action, composite_key);
|
||||
|
||||
// 호출할 저장 프로시저 쿼리
|
||||
let query = r"CALL sp_sync_resource_info_from_scan(
|
||||
:p_admin_user_id, :p_actor_description, :p_user_account_name,
|
||||
:p_category, :p_manufacturer, :p_resource_name, :p_serial_num,
|
||||
:p_spec_value, :p_spec_unit, :p_detected_by, :p_change_type,
|
||||
@p_result_message
|
||||
)"; // 총 11개 IN 파라미터 + 1개 OUT 파라미터
|
||||
|
||||
// 프로시저 파라미터 준비
|
||||
let params = params! {
|
||||
"p_admin_user_id" => Value::from(admin_user_id),
|
||||
"p_actor_description" => Value::from(actor_description),
|
||||
"p_user_account_name" => Value::from(user_account_name),
|
||||
"p_category" => Value::from(category),
|
||||
"p_manufacturer" => Value::from(manufacturer),
|
||||
"p_resource_name" => Value::from(resource_name),
|
||||
"p_serial_num" => Value::from(composite_key), // <-- 합성 키를 p_serial_num 파라미터로 전달
|
||||
"p_spec_value" => Value::from(spec_value), // 문자열로 전달
|
||||
"p_spec_unit" => Value::from(spec_unit), // 문자열로 전달
|
||||
"p_detected_by" => Value::from(detected_by),
|
||||
"p_change_type" => Value::from(change_type),
|
||||
};
|
||||
|
||||
// 로그: 프로시저 호출 직전 파라미터 확인
|
||||
debug!("Calling sp_sync_resource_info_from_scan with params: actor='{}', user='{}', category='{}', key='{}', change_type={}",
|
||||
actor_description, user_account_name, category, composite_key, change_type);
|
||||
|
||||
// 프로시저 실행 (결과를 반환하지 않음)
|
||||
conn.exec_drop(query, params).await.with_context(|| {
|
||||
format!(
|
||||
"sp_sync_resource_info_from_scan 프로시저 실행 실패: Actor='{}', 사용자='{}', 작업='{}', Key='{}'",
|
||||
actor_description, user_account_name, action, composite_key
|
||||
)
|
||||
})?;
|
||||
|
||||
// OUT 파라미터(@p_result_message) 값 가져오기
|
||||
let result_message_query = r"SELECT @p_result_message AS result_message";
|
||||
let result_row: Option<Row> = conn.query_first(result_message_query).await.with_context(|| {
|
||||
format!(
|
||||
"OUT 파라미터(@p_result_message) 읽기 실패: Actor='{}', 사용자='{}', Key='{}'",
|
||||
actor_description, user_account_name, composite_key
|
||||
)
|
||||
})?;
|
||||
|
||||
// 결과 메시지 처리
|
||||
let result_message = match result_row {
|
||||
Some(row) => {
|
||||
match row.get_opt::<String, _>("result_message") { // 컬럼 이름은 AS 로 지정한 이름 사용
|
||||
Some(Ok(msg)) => msg, // 성공적으로 문자열 얻음
|
||||
Some(Err(e)) => format!("결과 메시지 파싱 실패: {}", e), // 타입 변환 등 실패
|
||||
None => "결과 메시지가 NULL입니다".to_string(), // 컬럼 값 자체가 NULL
|
||||
}
|
||||
}
|
||||
None => "결과 메시지를 가져올 수 없음".to_string(), // 쿼리 결과 행이 없음
|
||||
};
|
||||
|
||||
// 로그: 작업 완료 및 결과 메시지 기록
|
||||
debug!("'{}'에 의한 '{}' 자원 동기화 ({}) 완료: Key='{}', 결과='{}'",
|
||||
actor_description, user_account_name, action, composite_key, result_message);
|
||||
|
||||
Ok(result_message)
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// 공개 함수 (main.rs 에서 호출)
|
||||
// ============================================================
|
||||
|
||||
// 비교 결과를 바탕으로 DB 동기화(추가/할당, 할당 해제)를 실행하는 함수
|
||||
pub async fn execute_sync(
|
||||
pool: &DbPool,
|
||||
hostname: &str, // 사용자 계정명 (user_account_name) 역할
|
||||
adds: Vec<ProcessedHwInfo>, // 추가/할당 대상 목록
|
||||
deletes: Vec<DbResource>, // 할당 해제 대상 목록
|
||||
) -> Result<()> {
|
||||
// 변경 사항 없으면 즉시 종료
|
||||
if adds.is_empty() && deletes.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 작업 주체 설명 정의 (로그 및 프로시저 파라미터용)
|
||||
let actor_description = format!("RustFileSync-{}", hostname);
|
||||
let admin_user_id: Option<i32> = None; // Rust 자동화 작업이므로 관리자 ID는 None
|
||||
|
||||
info!( "'{}': DB 동기화 시작 (추가/할당: {}개, 할당 해제: {}개)", &actor_description, adds.len(), deletes.len());
|
||||
|
||||
// DB 커넥션 가져오기
|
||||
let mut conn = pool.get_conn().await.context("DB 커넥션 얻기 실패 (sync)")?;
|
||||
|
||||
// --- 추가/할당 작업 루프 ---
|
||||
for item_to_add in adds {
|
||||
let log_key = item_to_add.serial_key.clone(); // 로그 및 전달용 합성 키
|
||||
|
||||
// 빈 합성 키 건너뛰기 (선택적이지만 권장)
|
||||
if item_to_add.serial_key.is_empty() {
|
||||
warn!("추가/할당 건너<0xEB><0x9B><0x81>: 유효하지 않은 합성 키. Host='{}', Category='{}', Model='{}'",
|
||||
hostname, item_to_add.category, item_to_add.model);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 동기화 프로시저 호출 (change_type = 1)
|
||||
match call_sync_procedure(
|
||||
&mut conn,
|
||||
admin_user_id,
|
||||
&actor_description,
|
||||
hostname, // user_account_name
|
||||
&item_to_add.category,
|
||||
&item_to_add.manufacturer,
|
||||
&item_to_add.model, // resource_name
|
||||
&item_to_add.serial_key, // composite_key (p_serial_num 파라미터)
|
||||
&item_to_add.spec_value,
|
||||
&item_to_add.spec_unit,
|
||||
hostname, // detected_by
|
||||
1, // change_type = ADD (추가 또는 할당)
|
||||
).await {
|
||||
Ok(msg) => {
|
||||
// 성공 로그 (결과 메시지 포함)
|
||||
info!("자원 추가/할당 결과: Key='{}', Msg='{}'", log_key, msg);
|
||||
// 결과 메시지에 따른 추가 경고 로깅
|
||||
if msg.contains("실패") || msg.contains("오류") || msg.contains("주의:") || msg.contains("다른 사용자") || msg.contains("이미 등록") {
|
||||
warn!("추가/할당 처리 중 특이사항: Key='{}', Msg='{}'", log_key, msg);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// 실패 로그
|
||||
error!("자원 추가/할당 실패: Key='{}', 오류: {}", log_key, e);
|
||||
// 여기서 에러를 반환할지, 아니면 계속 진행할지 결정 필요
|
||||
// return Err(e.context(format!("자원 추가/할당 실패: Key='{}'", log_key)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 할당 해제 작업 루프 ---
|
||||
for item_to_delete in deletes {
|
||||
let log_key = item_to_delete.serial_num.clone(); // 로그 및 전달용 합성 키 (DB에서 가져온 값)
|
||||
|
||||
// 빈 합성 키 건너뛰기 (DB에서 왔으므로 가능성은 낮음)
|
||||
if item_to_delete.serial_num.is_empty() {
|
||||
warn!("할당 해제 건너<0xEB><0x9B><0x81>: 유효하지 않은 합성 키. Host='{}', ID='{}'", hostname, item_to_delete.resource_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 동기화 프로시저 호출 (change_type = 2)
|
||||
match call_sync_procedure(
|
||||
&mut conn,
|
||||
admin_user_id,
|
||||
&actor_description,
|
||||
hostname, // user_account_name
|
||||
"", // category (할당 해제 시 불필요)
|
||||
"", // manufacturer (할당 해제 시 불필요)
|
||||
"", // resource_name (할당 해제 시 불필요)
|
||||
&item_to_delete.serial_num, // composite_key (p_serial_num 파라미터)
|
||||
"", // spec_value (할당 해제 시 불필요)
|
||||
"", // spec_unit (할당 해제 시 불필요)
|
||||
hostname, // detected_by
|
||||
2, // change_type = DELETE (할당 해제)
|
||||
).await {
|
||||
Ok(msg) => {
|
||||
// 성공 로그
|
||||
info!("자원 할당 해제 결과: Key='{}', Msg='{}'", log_key, msg);
|
||||
// 결과 메시지에 따른 추가 경고 로깅
|
||||
if msg.contains("실패") || msg.contains("오류") || msg.contains("주의:") || msg.contains("대상 없음") {
|
||||
warn!("할당 해제 처리 중 특이사항: Key='{}', Msg='{}'", log_key, msg);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// 실패 로그
|
||||
error!("자원 할당 해제 실패: Key='{}', 오류: {}", log_key, e);
|
||||
// 여기서 에러를 반환할지, 아니면 계속 진행할지 결정 필요
|
||||
// return Err(e.context(format!("자원 할당 해제 실패: Key='{}'", log_key)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("'{}': DB 동기화 완료.", &actor_description);
|
||||
Ok(())
|
||||
}
|
||||
106
apps/rust_gyber/src/file/decrypt.rs
Normal file
106
apps/rust_gyber/src/file/decrypt.rs
Normal file
@ -0,0 +1,106 @@
|
||||
// src/file/decrypt.rs
|
||||
// 내부 전용 어플인데 구지 필요할까 싶지만 그냥 스터디 차원에서 별도의 암호화/복호화 기능 구현.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize; // derive 매크로가 Deserialize 트레잇 사용
|
||||
use std::{env, fs};
|
||||
use std::path::Path;
|
||||
|
||||
// --- AES-GCM 관련 의존성 ---
|
||||
use aes_gcm::aead::Aead; // Aead 트레잇은 그대로 사용
|
||||
use aes_gcm::KeyInit; // KeyInit 트레잇을 직접 임포트
|
||||
use aes_gcm::{Aes128Gcm, Key, Nonce}; // 나머지 타입들
|
||||
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine as _};
|
||||
|
||||
// 애플리케이션에서 사용할 DB 접속 정보 구조체
|
||||
#[derive(Deserialize, Debug, Clone)] // Deserialize 트레잇 사용 명시
|
||||
pub struct DbCredentials {
|
||||
pub host: String,
|
||||
pub user: String,
|
||||
pub pass: String,
|
||||
pub db: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
// 암호화된 JSON 파일 내용에 맞는 임시 구조체
|
||||
#[derive(Deserialize, Debug)] // Deserialize 트레잇 사용 명시
|
||||
struct EncryptedJsonData {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
// 환경 변수에서 암호화 키 로드
|
||||
fn load_encryption_key() -> Result<Key<Aes128Gcm>> {
|
||||
let key_b64 = env::var("DB_ENCRYPTION_KEY")
|
||||
.context("환경 변수 'DB_ENCRYPTION_KEY'를 찾을 수 없습니다.")?;
|
||||
let key_bytes = base64_engine.decode(key_b64.trim())
|
||||
.context("DB_ENCRYPTION_KEY Base64 디코딩 실패")?;
|
||||
if key_bytes.len() == 16 {
|
||||
Ok(*Key::<Aes128Gcm>::from_slice(&key_bytes))
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"DB_ENCRYPTION_KEY 키 길이가 16바이트가 아닙니다 (현재 {}바이트).", key_bytes.len()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 환경 변수에서 DB 접속 정보(host, name, port) 로드
|
||||
fn load_db_connection_info() -> Result<(String, String, u16)> {
|
||||
let host = env::var("DB_HOST").context("환경 변수 'DB_HOST'를 찾을 수 없습니다.")?;
|
||||
let db_name = env::var("DB_NAME").context("환경 변수 'DB_NAME'를 찾을 수 없습니다.")?;
|
||||
let port_str = env::var("DB_PORT").context("환경 변수 'DB_PORT'를 찾을 수 없습니다.")?;
|
||||
let port = port_str
|
||||
.parse::<u16>()
|
||||
.with_context(|| format!("DB_PORT 값 '{}' 파싱 실패", port_str))?;
|
||||
Ok((host, db_name, port))
|
||||
}
|
||||
|
||||
// 암호화된 DB 설정 파일을 읽고 복호화하는 함수 (AES-GCM)
|
||||
pub fn decrypt_db_config(db_config_path: &str) -> Result<DbCredentials> {
|
||||
log::info!("'{}' 파일 복호화 시도 (AES-GCM)...", db_config_path);
|
||||
let path = Path::new(db_config_path);
|
||||
if !path.exists() {
|
||||
anyhow::bail!("DB 설정 파일을 찾을 수 없습니다: {}", db_config_path);
|
||||
}
|
||||
|
||||
let key = load_encryption_key().context("암호화 키 로드 실패")?;
|
||||
log::debug!("암호화 키 로드 성공.");
|
||||
|
||||
let encrypted_data_with_nonce = fs::read(path)
|
||||
.with_context(|| format!("DB 설정 파일 읽기 실패: {}", db_config_path))?;
|
||||
|
||||
let nonce_len = 12; // AES-GCM 표준 Nonce 길이
|
||||
if encrypted_data_with_nonce.len() <= nonce_len {
|
||||
anyhow::bail!("암호화된 데이터가 너무 짧습니다.");
|
||||
}
|
||||
let (nonce_bytes, ciphertext) = encrypted_data_with_nonce.split_at(nonce_len);
|
||||
let nonce = Nonce::from_slice(nonce_bytes);
|
||||
log::debug!("Nonce와 암호문 분리 완료.");
|
||||
|
||||
let cipher = Aes128Gcm::new(&key); // NewAead 트레잇 사용
|
||||
let decrypted_bytes = cipher.decrypt(nonce, ciphertext) // Aead 트레잇 사용
|
||||
.map_err(|e| anyhow::anyhow!("AES-GCM 복호화 실패: {}", e))?;
|
||||
log::debug!("AES-GCM 복호화 및 인증 성공.");
|
||||
|
||||
// Deserialize 트레잇 사용
|
||||
let temp_data: EncryptedJsonData = serde_json::from_slice(&decrypted_bytes)
|
||||
.context("복호화된 데이터 JSON 파싱 실패 (EncryptedJsonData)")?;
|
||||
log::debug!("복호화된 JSON 데이터 파싱 성공 (Username: {}).", temp_data.username);
|
||||
|
||||
let (host, db_name, port) = load_db_connection_info()
|
||||
.context("DB 연결 정보 환경 변수 로드 실패")?;
|
||||
log::debug!("DB 연결 정보 로드 성공.");
|
||||
|
||||
let db_creds = DbCredentials {
|
||||
host,
|
||||
db: db_name,
|
||||
port,
|
||||
user: temp_data.username,
|
||||
pass: temp_data.password,
|
||||
};
|
||||
|
||||
log::info!("DB 설정 파일 복호화 및 전체 인증 정보 구성 완료 (Host: {}, User: {}, DB: {}, Port: {}).",
|
||||
db_creds.host, db_creds.user, db_creds.db, db_creds.port);
|
||||
|
||||
Ok(db_creds)
|
||||
}
|
||||
192
apps/rust_gyber/src/file/json_reader.rs
Normal file
192
apps/rust_gyber/src/file/json_reader.rs
Normal file
@ -0,0 +1,192 @@
|
||||
use anyhow::{Context, Result};
|
||||
use log::{debug, error, info, warn};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
// 문자열 정리 및 기본값("N/A") 설정 함수
|
||||
fn clean_string(s: Option<&str>) -> String {
|
||||
match s {
|
||||
Some(val) => {
|
||||
let trimmed = val.trim();
|
||||
if trimmed.is_empty() {
|
||||
"N/A".to_string() // 비어 있으면 "N/A" 반환
|
||||
} else {
|
||||
trimmed.to_string() // 앞뒤 공백 제거
|
||||
}
|
||||
}
|
||||
None => "N/A".to_string(), // Option이 None이면 "N/A" 반환
|
||||
}
|
||||
}
|
||||
|
||||
// JSON 파일의 원본 데이터 구조체
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct RawHwInfo {
|
||||
pub hostname: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub manufacturer: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub serial: Option<String>, // 실제 시리얼 (합성 키 생성에 사용될 수 있음)
|
||||
pub spec_value: Option<String>,
|
||||
pub spec_unit: Option<String>,
|
||||
pub port_or_slot: Option<String>, // 합성 키 생성에 사용
|
||||
}
|
||||
|
||||
// 처리된 하드웨어 정보 구조체 (실제 사용될 데이터)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProcessedHwInfo {
|
||||
pub hostname: String,
|
||||
pub category: String,
|
||||
pub manufacturer: String,
|
||||
pub model: String,
|
||||
pub serial_key: String, // <-- 합성 키 (비교 및 DB 전달용)
|
||||
pub spec_value: String,
|
||||
pub spec_unit: String,
|
||||
}
|
||||
|
||||
// 지정된 디렉토리에서 패턴에 맞는 JSON 파일 목록 찾기
|
||||
pub fn find_json_files(dir_path: &str) -> Result<Vec<PathBuf>> {
|
||||
let mut json_files = Vec::new();
|
||||
let path = Path::new(dir_path);
|
||||
|
||||
// 경로 유효성 검사
|
||||
if !path.is_dir() {
|
||||
anyhow::bail!("제공된 JSON 경로가 디렉토리가 아닙니다: {}", dir_path);
|
||||
}
|
||||
|
||||
// 디렉토리 순회하며 파일 찾기
|
||||
for entry in fs::read_dir(path).with_context(|| format!("디렉토리 읽기 오류: {}", dir_path))? {
|
||||
let entry = entry.context("디렉토리 항목 읽기 실패")?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
// 파일 이름 패턴 확인 (HWInfo_*.json)
|
||||
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if filename.starts_with("HWInfo_") && filename.ends_with(".json") {
|
||||
debug!("발견된 JSON 파일: {:?}", path);
|
||||
json_files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(json_files)
|
||||
}
|
||||
|
||||
// 단일 JSON 파일 파싱 및 데이터 처리 함수
|
||||
fn parse_and_process_single_json(path: &Path) -> Result<Vec<ProcessedHwInfo>> {
|
||||
// 파일 읽기 및 BOM 처리
|
||||
let bytes = fs::read(path).with_context(|| format!("JSON 파일 읽기 실패: {:?}", path))?;
|
||||
let json_content_without_bom = if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
|
||||
std::str::from_utf8(&bytes[3..]).with_context(|| format!("UTF-8 변환 실패(BOM제거): {:?}", path))?
|
||||
} else {
|
||||
std::str::from_utf8(&bytes).with_context(|| format!("UTF-8 변환 실패: {:?}", path))?
|
||||
};
|
||||
|
||||
// 빈 파일 처리
|
||||
if json_content_without_bom.trim().is_empty() {
|
||||
warn!("JSON 파일 내용이 비어 있습니다: {:?}", path);
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// JSON 파싱
|
||||
let raw_data: Vec<RawHwInfo> = serde_json::from_str(json_content_without_bom)
|
||||
.with_context(|| format!("JSON 파싱 실패: {:?}", path))?;
|
||||
|
||||
let mut processed_list = Vec::new();
|
||||
// 처리할 카테고리 목록 (필요시 DB와 동기화 또는 설정 파일로 분리)
|
||||
let relevant_categories = ["CPU", "Mainboard", "Memory", "SSD", "HDD", "VGA"];
|
||||
|
||||
for raw_item in raw_data {
|
||||
// 필수 필드(카테고리, 호스트명) 확인
|
||||
if let (Some(cat_str), Some(host_str)) = (raw_item.category.as_deref(), raw_item.hostname.as_deref()) {
|
||||
let category = clean_string(Some(cat_str));
|
||||
let hostname = clean_string(Some(host_str));
|
||||
|
||||
// 관련 없는 카테고리 또는 유효하지 않은 호스트명 건너뛰기
|
||||
if hostname == "N/A" || !relevant_categories.contains(&category.as_str()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let original_serial = clean_string(raw_item.serial.as_deref());
|
||||
let port_or_slot = clean_string(raw_item.port_or_slot.as_deref());
|
||||
let model = clean_string(raw_item.model.as_deref());
|
||||
|
||||
// --- 합성 키 생성 로직 ---
|
||||
let serial_key: String;
|
||||
let mut key_parts: Vec<String> = Vec::new();
|
||||
// 실제 시리얼, 슬롯/포트, 호스트명을 조합하여 고유 키 생성 시도
|
||||
if original_serial != "N/A" { key_parts.push(original_serial.replace('.', "")); }
|
||||
if port_or_slot != "N/A" { key_parts.push(port_or_slot.replace('.', "")); }
|
||||
if hostname != "N/A" { key_parts.push(hostname.replace('.', "")); }
|
||||
let combined_key = key_parts.iter().map(AsRef::as_ref).collect::<Vec<&str>>().join("_");
|
||||
|
||||
// 조합된 키가 비어있거나 호스트명만 있는 경우, 대체 키 생성 시도 (모델명+호스트명)
|
||||
if combined_key.is_empty() {
|
||||
let model_cleaned = model.replace('.', "");
|
||||
let hostname_cleaned_fallback = hostname.replace('.', "");
|
||||
if model != "N/A" && hostname != "N/A" {
|
||||
serial_key = format!("{}_{}", model_cleaned, hostname_cleaned_fallback);
|
||||
debug!("대체 Serial Key 생성 (모델+호스트): {}", serial_key);
|
||||
} else {
|
||||
// 대체 키 생성도 불가능하면 해당 항목 건너뛰기
|
||||
warn!("고유 Key 생성 불가 (시리얼/슬롯/모델 정보 부족), 항목 건너<0xEB><0x9B><0x81>: {:?}", raw_item);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
serial_key = combined_key;
|
||||
}
|
||||
// --- 합성 키 생성 로직 끝 ---
|
||||
|
||||
// 처리된 데이터 구조체 생성
|
||||
let processed = ProcessedHwInfo {
|
||||
hostname: hostname.clone(),
|
||||
category, // clean_string 이미 적용됨
|
||||
manufacturer: clean_string(raw_item.manufacturer.as_deref()),
|
||||
model, // clean_string 이미 적용됨
|
||||
serial_key, // 생성된 합성 키
|
||||
spec_value: clean_string(raw_item.spec_value.as_deref()),
|
||||
spec_unit: clean_string(raw_item.spec_unit.as_deref()),
|
||||
};
|
||||
processed_list.push(processed);
|
||||
} else {
|
||||
// 필수 필드 누락 시 경고 로그
|
||||
warn!("필수 필드(Category 또는 Hostname) 누락 항목 건너<0xEB><0x9B><0x81>: {:?}", raw_item);
|
||||
}
|
||||
}
|
||||
Ok(processed_list)
|
||||
}
|
||||
|
||||
// 모든 JSON 파일을 읽고 처리하여 호스트명 기준으로 그룹화하는 함수
|
||||
pub fn read_and_process_json_files(
|
||||
json_dir_path: &str,
|
||||
) -> Result<HashMap<String, Vec<ProcessedHwInfo>>> {
|
||||
info!("'{}' 디렉토리에서 JSON 파일 검색 및 처리 시작...", json_dir_path);
|
||||
let json_files = find_json_files(json_dir_path)?;
|
||||
info!("총 {}개의 JSON 파일 발견.", json_files.len());
|
||||
|
||||
let mut all_processed_data: HashMap<String, Vec<ProcessedHwInfo>> = HashMap::new();
|
||||
// 각 JSON 파일 처리
|
||||
for json_path in json_files {
|
||||
debug!("처리 중인 파일: {:?}", json_path);
|
||||
match parse_and_process_single_json(&json_path) {
|
||||
Ok(processed_items) => {
|
||||
if !processed_items.is_empty() {
|
||||
// 처리된 데이터를 호스트명 기준으로 그룹화
|
||||
for item in processed_items {
|
||||
all_processed_data.entry(item.hostname.clone()).or_default().push(item);
|
||||
}
|
||||
} else {
|
||||
// 처리할 데이터가 없는 경우 디버그 로그
|
||||
debug!("파일에서 처리할 관련 카테고리 데이터가 없습니다: {:?}", json_path);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// 파일 처리 중 오류 발생 시 에러 로그
|
||||
error!("파일 처리 중 오류 발생 {:?}: {}", json_path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("JSON 파일 처리 완료. 총 {}개 호스트 데이터 생성.", all_processed_data.len());
|
||||
Ok(all_processed_data)
|
||||
}
|
||||
5
apps/rust_gyber/src/file/mod.rs
Normal file
5
apps/rust_gyber/src/file/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
// src/file/mod.rs
|
||||
pub mod json_reader;
|
||||
pub mod decrypt;
|
||||
|
||||
// main.rs에서 필요한 것들 위주로 내보내기
|
||||
27
apps/rust_gyber/src/logger/logger.rs
Normal file
27
apps/rust_gyber/src/logger/logger.rs
Normal file
@ -0,0 +1,27 @@
|
||||
// src/logger/logger.rs
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
// use log::LevelFilter; // 사용하지 않으므로 제거
|
||||
use log4rs::init_file;
|
||||
use std::fs;
|
||||
|
||||
// log4rs 설정 파일을 읽어 로거를 초기화하는 함수
|
||||
pub fn setup_logger() -> Result<()> {
|
||||
let log_dir = "logs";
|
||||
let config_file = "config/log4rs.yaml";
|
||||
|
||||
// 1. 로그 디렉토리 생성 (없으면)
|
||||
if !std::path::Path::new(log_dir).exists() {
|
||||
fs::create_dir_all(log_dir)
|
||||
.with_context(|| format!("로그 디렉토리 '{}' 생성 실패", log_dir))?;
|
||||
println!("로그 디렉토리 '{}' 생성됨.", log_dir); // 로거 초기화 전
|
||||
}
|
||||
|
||||
// 2. log4rs 설정 파일 로드 및 초기화
|
||||
init_file(config_file, Default::default())
|
||||
.with_context(|| format!("log4rs 설정 파일 '{}' 로드 및 초기화 실패", config_file))?;
|
||||
|
||||
log::info!("Logger initialized using config file: {}", config_file); // 로거 초기화 후
|
||||
|
||||
Ok(())
|
||||
}
|
||||
7
apps/rust_gyber/src/logger/mod.rs
Normal file
7
apps/rust_gyber/src/logger/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
// src/logger/mod.rs
|
||||
|
||||
// logger 모듈을 현재 모듈(logger)의 하위 모듈로 선언
|
||||
pub mod logger;
|
||||
|
||||
// logger 모듈의 setup_logger 함수를 외부에서 logger::setup_logger 형태로 사용할 수 있도록 공개 (re-export)
|
||||
pub use logger::setup_logger;
|
||||
129
apps/rust_gyber/src/main.rs
Normal file
129
apps/rust_gyber/src/main.rs
Normal file
@ -0,0 +1,129 @@
|
||||
use anyhow::{Context, Result};
|
||||
use log::{error, info, warn};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// --- 애플리케이션 모듈 선언 ---
|
||||
mod config_reader;
|
||||
mod db; // 데이터베이스 관련 모듈 (connection, compare, sync 포함)
|
||||
mod file; // 파일 처리 관련 모듈 (json_reader, decrypt 포함)
|
||||
mod logger;
|
||||
|
||||
// --- 필요한 구조체 및 함수 임포트 ---
|
||||
use config_reader::read_app_config;
|
||||
use db::{
|
||||
compare::{compare_data, ComparisonResult}, // 데이터 비교 함수 및 결과 구조체
|
||||
connection::connect_db, // DB 연결 함수
|
||||
sync::execute_sync, // DB 동기화 실행 함수
|
||||
};
|
||||
use file::{
|
||||
decrypt::decrypt_db_config, // DB 설정 복호화 함수
|
||||
json_reader::{read_and_process_json_files, ProcessedHwInfo}, // JSON 처리 함수 및 구조체
|
||||
};
|
||||
use logger::setup_logger; // 로거 설정 함수
|
||||
|
||||
// --- 애플리케이션 메인 진입점 ---
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// .env 파일 로드 (환경 변수 사용 위함, 예: 복호화 키)
|
||||
dotenvy::dotenv().ok(); // 파일 없어도 오류 아님
|
||||
|
||||
// 1. 로거 초기화
|
||||
setup_logger().context("로거 설정 실패")?;
|
||||
info!("자원 관리 동기화 애플리케이션 시작...");
|
||||
info!("환경 변수 로드 시도 완료."); // 실제 로드 여부는 dotenvy 결과 확인 필요
|
||||
|
||||
// 2. 애플리케이션 설정 파일 읽기
|
||||
let config_path = "config/config.json"; // 설정 파일 경로
|
||||
info!("설정 파일 읽기 시도: {}", config_path);
|
||||
let app_config = read_app_config(config_path).context("애플리케이션 설정 읽기 실패")?;
|
||||
info!("설정 로드 완료: JSON 경로='{}', DB 설정 파일='{}'", app_config.json_files_path, app_config.db_config_path);
|
||||
|
||||
// 3. DB 인증 정보 복호화
|
||||
info!("DB 설정 파일 복호화 시도: {}", app_config.db_config_path);
|
||||
let db_creds = decrypt_db_config(&app_config.db_config_path).context("DB 인증 정보 복호화 실패")?;
|
||||
info!("DB 설정 복호화 완료."); // 성공 로그 (민감 정보 노출 주의)
|
||||
|
||||
// 4. 데이터베이스 연결 풀 생성
|
||||
info!("데이터베이스 연결 시도: 호스트={}, DB={}", db_creds.host, db_creds.db);
|
||||
let db_pool = connect_db(&db_creds).await.context("데이터베이스 연결 풀 생성 실패")?;
|
||||
info!("데이터베이스 연결 풀 생성 완료.");
|
||||
|
||||
// 5. JSON 파일 읽기 및 처리
|
||||
let processed_data: HashMap<String, Vec<ProcessedHwInfo>> =
|
||||
match read_and_process_json_files(&app_config.json_files_path) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
// JSON 처리 실패 시 즉시 종료
|
||||
error!("JSON 파일 처리 실패: {}", e);
|
||||
return Err(e.context("JSON 파일 처리 중 치명적 오류 발생"));
|
||||
}
|
||||
};
|
||||
|
||||
// 처리할 데이터가 없는 경우 종료
|
||||
if processed_data.is_empty() {
|
||||
warn!("처리할 유효한 JSON 데이터 없음. 종료.");
|
||||
return Ok(());
|
||||
}
|
||||
info!("총 {}개 호스트 데이터 처리 완료.", processed_data.len());
|
||||
|
||||
// 6. 데이터 비교 (JSON vs DB)
|
||||
let comparison_results: HashMap<String, ComparisonResult> =
|
||||
match compare_data(&db_pool, &processed_data).await {
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
// 데이터 비교 실패 시 즉시 종료
|
||||
error!("데이터 비교 실패: {}", e);
|
||||
return Err(e.context("데이터 비교 중 치명적 오류 발생"));
|
||||
}
|
||||
};
|
||||
|
||||
// 변경 사항 없는 경우 종료
|
||||
if comparison_results.is_empty() {
|
||||
info!("DB와 비교 결과, 변경 사항 없음. 종료.");
|
||||
return Ok(());
|
||||
}
|
||||
info!("총 {}개 호스트 변경 사항 발견.", comparison_results.len());
|
||||
|
||||
// 7. DB 동기화 실행 (추가/할당, 할당 해제)
|
||||
info!("DB 동기화 작업 시작...");
|
||||
let mut success_count = 0;
|
||||
let mut fail_count = 0;
|
||||
let total_hosts_to_sync = comparison_results.len();
|
||||
|
||||
// 각 호스트별 변경 사항 DB에 적용
|
||||
for (hostname, changes) in comparison_results {
|
||||
info!("'{}' 호스트 DB 동기화 처리 중...", hostname);
|
||||
match execute_sync(
|
||||
&db_pool,
|
||||
&hostname,
|
||||
changes.adds, // 추가/할당 대상 전달
|
||||
changes.deletes // 할당 해제 대상 전달
|
||||
).await {
|
||||
Ok(_) => {
|
||||
// 성공 시 카운트 증가
|
||||
success_count += 1;
|
||||
info!("'{}' 호스트 DB 동기화 성공.", hostname);
|
||||
}
|
||||
Err(e) => {
|
||||
// 실패 시 카운트 증가 및 에러 로그
|
||||
fail_count += 1;
|
||||
error!("'{}' 호스트 DB 동기화 에러: {}", hostname, e);
|
||||
// 개별 호스트 실패 시 전체 프로세스를 중단할지, 아니면 계속 진행할지 결정
|
||||
// 여기서는 계속 진행하고 마지막에 요약
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8. 최종 결과 요약 로깅
|
||||
info!("--- DB 동기화 작업 요약 ---");
|
||||
info!("총 대상 호스트: {}", total_hosts_to_sync);
|
||||
info!("성공 처리 호스트: {}", success_count);
|
||||
info!("오류 발생 호스트: {}", fail_count);
|
||||
if fail_count > 0 {
|
||||
// 실패한 호스트가 있으면 에러 레벨 로그 추가
|
||||
error!("일부 호스트 동기화 중 오류 발생. 상세 내용은 위 로그 확인 필요.");
|
||||
}
|
||||
info!("자원 관리 동기화 애플리케이션 정상 종료.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user