convert to gitea

This commit is contained in:
2025-09-15 13:33:34 +09:00
commit 95882ac072
277 changed files with 46023 additions and 0 deletions

12
apps/rust_gyber/.env Normal file
View 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

File diff suppressed because it is too large Load Diff

View 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"] }

View File

@ -0,0 +1,4 @@
{
"json_file_path": "/pcinfo",
"db_config_path": "config/db.enc"
}

View 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

View 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면 이 설정만 적용

View 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)
}

View 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)
}

View 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
))
}
}
}

View 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;

View 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(())
}

View 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)
}

View 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)
}

View File

@ -0,0 +1,5 @@
// src/file/mod.rs
pub mod json_reader;
pub mod decrypt;
// main.rs에서 필요한 것들 위주로 내보내기

View 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(())
}

View 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
View 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(())
}