도움받은 사이트 :

https://developer.zebra.com/thread/34694

https://ashuro.tistory.com/20

https://stackoverflow.com/questions/68627676/c-sharp-zebra-hex-compress-algorithm

 

Zebra ZPL 에서 GFA 명령어는 이미지를 라벨에 출력할 때 사용하는 Command 이다.

 

labelary 사이트에서 간단히 가져다 쓸 수도 있지만

최대한 개념을 익히는 방향으로 글을 쓴다.

 

♣ BMP 파일 생성

테스트를 위해 BMP 파일을 하나 만든다.

Zebra 프린터의 경우, 흑백으로 출력하기에 BMP 저장 시, 1비트로 저장한다.

 

아래는 Paint.net (무료) 이라는 이미지툴에서 아래처럼 적당히 이미지를 만들고,

1비트로 저장하는 예시이다.

디더링 수준은 테스트 결과, 무시해도 된다.

 

♣ Labelary 사이트에서 GFA 데이터 얻기

1. Labelary 사이트에 접속한다. ( Link : https://labelary.com/viewer.html  )

2. 아래처럼 ZPL 코드창에 ^XA ^XZ 명령어를 입력한다.

3. 위와 같이 커서를 두고, Add Image 버튼을 눌러 위에서 생성한 BMP 파일을 부르면,

아래처럼 GFA 코드를 생성한다.

 

4. 이제 위에 생성된 코드값을 적절히 사용하면 된다.

5. 끝 ㅋㅋ~

6. 이제 나가세요. ㅎㅎ

 

실제 위와 같이 쓰는 경우도 봤지만 매번 저 사이트에서 이미지를 불러와 값을 얻어야 한다.

아래는 위 값이 어떻게 얻어지는지에 대해 기술한다.

 

♣ BMP Color Table

BMP 정보에 Color Table 값이 있는데 이 값에 따라 0 -> 1 또는 1 -> 0 으로 변환시켜야 한다.

예를 들어 아래와 같은 사각형은,

Color Table 값에 따라 역상되어 저장될 수 있다. 이 경우, 0 -> 1로 바꿔주는 작업이 필요하다.

이게, 디자인팀으로부터 이미지를 받았을 때 역상되어 출력되어 우연히 발견한 경우인데,

디자이너에게 물어봐도 자기도 모른다는 답변만...

 

일단은 무식하게, Color Table 값으로 위와 같이 두가지로 저장될 수 있다고 이해하자.

 

♣ 1비트 BMP Raw Data

앞서 언급했듯이 비트 단위를 1비트로 두고 저장하면 실제 데이터는 비트 단위로 저장되게 된다.

하지만 파일은 바이트 단위로 저장된다.

이에 비트 단위 데이터가 어떻게 저장되는지에 대한 이해가 필요하다.

 

예를 들어 6 x 5 크기의 bmp 파일이 있다고 하자.

이 파일을 hex editor 로 열면 아래가 Data 영역이 된다.

 

Raw Data 만 나열하면 아래와 같다. 높이가 5이니 5줄로 표시될 것이다.

(BMP Data 는 역행으로 저장되므로 맨 윗줄이 맨 아래 저장된다. 맨 아랫줄은 맨 위에 저장됨)

 

위에서 보는 것처럼 폭이 6 픽셀이지만 한줄은 32비트, 4바이트를 차지하고 26비트가 0으로 Padding 되었다.

 

만약 크기가 30  x 5 이면 어떨까?

한줄은 32비트 (4Bytes) 가 사용되며 우측 2비트는 Padding 되었다.

이제 추측이 가능하다.

이미지의 폭은 32비트 단위로 나뉘어지며 남는 비트는 0 으로 padding 되는 것을~

 

이를 위해 38 x 5  짜리의 Raw Data 를 확인해 보면 아래처럼 64비트, 8바이트로 확장됨을 확인할 수 있다.

 

♣ GFA 값 생성

Zebra 문서에 따르면 GF Command 는 아래처럼 정의된다.

a : Compression Type

    A : ASCII Hex 값. 위 예제처럼 Hex 텍스트로 읽을 수 있는 값. 우리가 사용하는 값

    B : Binary - 말그대로 binary 그 자체. 메모장으로 열면 깨져서 보인다.

    C : Compressed Binary - 위 binary 를 압축한 것 같다. 자세한 건 문서 참조.

b : Binary byte count

c : Graphic field count

d : bytes per row

data : data

 

이 절에선 b,c,d 값 의 계산을 다룬다.

글 앞부분에서 Labelary 사이트에서 "Add Image" 로 bmp 파일을 불러오면

자동으로 GFA 코드가 생성되는 것을 확인할 수 있었다.

 

bmp 이미지 크기별로 생성되는 GFA 코드는 아래와 같다.

(Data 영역은 나중에 다룰 것이며, 여기선 b,c,d 값에 중점을 둔다. )

이미지 크기 (Pixel) ^GFA 코드 b
(Binary Byte Count)
c
(Graphic Field Count)
d
(bytes per row)
6 x 5 ^GFA,5,5,1, 5 5 1
14 x 5 ^GFA,10,10,2 10 10 2
22 x 5 ^GFA,15,15,3 15 15 3
24 x 5 ^GFA,15,15,3, 15 15 3
30 x 5 ^GFA,20,20,4 20 20 4
38 x 5 ^GFA,25,25,5 25 25 5

 

일단 눈대중으로 확인할 수 있는 내용은,

  • b,c 값을 동일하다. ( 아마도 GFB, GFC 인 경우, 두 값이 다를 것 같다.)
  • d 값은 이미지의 폭을 8 로 나눈 몫이다. (int)width / 8

그렇다면 b 값만 구하면 된다.

우선, 이미지 폭 (width) 의 정의를 아래처럼 3가지로 나눌 수 있다. (용어는 내마음대로 정함)

이 때, b,d 값은

  • b = RealWidth * Rows = 1 * 5 = 5
  • d = RealWidth = 1

가 된다.

 

만약 30 x 5 픽셀이라면, 아래처럼 계산될 것이다.

 

이 때 b,d 값은

  • b = RealWidth * Rows = 4 * 5 = 20
  • d = RealWidth = 4

가 된다.

 

♣ GFA 옵션 b,c,d 계산

그림상으로는 위와 같이 계산이 되는데 실제 코드상에선  아래처럼 구할 수 있다.

우선, BMP 헤더 구조체에서 구할 수 있는 정보는 아래와 같다.

(아래 구조체 변수는 윈도우 기준이다.)

 

BITMAPFILEHEADER bmFile ;

  • bmFile.bfSize : 파일 크기
  • bmFile.bfOffBits : 파일에서 Raw Data 가 시작되는 위치

BITMAPINFOHEADER bmInfo ;

  • bmInfo.biWidth : 실제 이미지의 가로(폭) 사이즈
  • bmInfo.biHeight : 실제 이미지의 세로(높이, rows) 사이즈

이 때, 앞절 그림의 4가지 값은 아래처럼 구할 수 있다.

  • bmInfo.biWidth = bmInfo.biWidth
  • bmInfo.biHeight = bmInfo.biHeight
  • Width = (bmFile.bfSize - bmFile.bfOffBits) /  bmInfo.biHeight
  • RealWidth = (bmInfo.biWidth + 7) / 8

이제 최종으로 GFA 명령어의 옵션 b,c,d 값은 아래처럼 계산된다.

  • b = RealWidth * bmInfo.biHeight
  • c = b
  • d = RealWidth

♣ GFA 옵션 data 얻기 - 정상과 역상

앞서 언급했듯이 Color Table 에 따라 Raw Data 는 아래처럼 정상 또는 역상으로 저장되어 있다.

역상인 경우, 1->0, 0->1 로 뒤집어줘야 한다. (Exclusive OR )

 

♣ GFA 옵션 data 얻기 - 정상

아래는 14 x 5 크기의 정상 Raw Data 영역이다.

 

위의 경우, Real Width = 2 Bytes 만큼만 취하고 나머지는 버린다.

크기가 14 이므로 16 - 14 = 2 bits 만큼 0 으로 채운다.

그리고 남은 데이터가 GFA 의 마지막 data 옵션에 들어간다.

위 그림의 경우,

  • FFFCC38472E41A3C0E0C

가 최종 data 가 된다.

이 data 만으로도 이미지를 출력할 수 있지만 압축하여 data 의 길이를 줄일 수 있다.

이는 저~ 아래에서 다룬다.

 

위 데이터값을 사용하여 아래와 같이 ZPL 을 구성할 수 있다.

Labelary 에서 정상적으로 bitmap 을 출력하는 것을 확인할 수 있다.

ZPL 라벨 이미지
^XA
^FO10,10
^GFA,10,10,2,FFFCC38472E41A3C0E0C
^XZ

 

 

♣ GFA 옵션 data 얻기 - 역상

위 이미지의 역상 Raw Data는 아래와 같다.

정상인 경우와 마찬가지로 우측 2 Bytes 를 버린다.

그리고 정상과는 달리 1로 Padding bits 를 채운다.

 

데이터 버리고 Padding Bits 를 1로 채우면 아래와 같다.
그리고 Exclusive OR (XOR) 연산을 하면 정상과 같은 데이터를 얻을 수 있다.

이 후, 정상과 같은 절차로 GFA 를 생성할 수 있다. (정상과 같으므로 생략)

 

♣ Raw Data 압축

테스트를 위해 아래와 같이 임의의 1bits 짜리 bmp 파일을 생성했다.

 

그리고 위 절차와 같이 GFA 명령어용 data 를 추출하면 아래와 같다.

  • AAAEE000AAAEE000AAAEE0000000000000000000FC3FE78087E73C00

그런데 위 이미지 파일을 labelary 사이트에서 불러오면 data 영역이 아래와 같다.

  • IAEE,::,:FC3FE78,87E73C,

두 결과는 같다.

즉, 위 56글자를 아래 24글자로 줄일 수 있다는 것이다.

 

규칙은 아래와 같다.

 

■ 연속된 글자

연속된 글자는 "갯수 + 글자" 로 줄인다. 예를 들면,

Raw Data 압축 설명
444444 L4 L=6번 반복 -> 4를 6번 찍음
DDD ID I=3번 반복 -> D를 3번 찍음
C -> 340 개 연속 wC w=340번 반복
C -> 342개 wHC w=340번 + H=2번 만큼 반복
F -> 495개 zFjUF z(400) + j (80) + U (15)  = 495 개

 

반복 횟수를 나타내는 앞글자는 아래처럼 정의된다.

HEX 값에 사용되는 A~F 를 벗어난 값으로 세팅됨을 알 수 있다.

G, H 의 경우, 20회 이상인 경우에 사용된다.

반복횟수가 1인 경우 -> 압축하면 손해 (1글자 -> 2글자)

반복횟수가 2인경우 -> 압축의 의미없음 (2글자 -> 2글자)

400개 이상인 경우, 400개에서 끊는다.

G : 1 회 (20회 이상)
H : 2 회 (20회 이상)
I : 3 회
J : 4 회
K : 5
L : 6
M : 7
N : 8
O : 9
P : 10

Q : 11 회
R : 12 회
S : 13 회
T : 14
U : 15
V : 16
W : 17
X : 18
Y : 19
g : 20회
h : 40회
i : 60회
j : 80회
k : 100
l : 120
m : 140
n : 160
o : 180
p : 200
q : 220 회
r : 240
s : 260
t : 280
u : 300
v : 320
w : 340
x : 360
y : 380
z : 400

 

■ 한 줄 끝까지 0인 경우,

예를 들어 Data의 뒷부분이 0 이면 ,(콤마) 로 치환할 수 있다.

Raw Data 압축 설명
DA0000 DA,  
000000 , 한줄 전체가 ',' 로 압축됨
555F00 I5F, 5 세번 + 'F' + ','

 

■ 한 줄 끝까지 F인 경우,

위와 맥락이 같으며 뒷부분이 F이면, ! (느낌표) 로 치환할 수 있다.

내용은 위 0 과 동일하다.

 

■ 윗 줄과 같은 경우

윗줄과 내용이 같으면 : (콜론) 으로 치환할 수 있다. 

Raw Data 압축 설명
DA0000
DA0000
DA,: 두번째 줄이 윗줄과 같으므로 ':' 으로 압축됨
DA0000
DA0000
DA0000
DA0000
DA,::: 뒤에 세줄 반복이므로
':' 를 세번 사용하여 표시함.

 

 

위와 같이 압축기법을 적용하면, 위 25x7 예제 BMP 파일의 raw data 는 아래처럼 압축될 것이다.

최종으로 얻은 값은 Labelary 사이트에서 생성된 값이랑 일치함을 확인할 수 있다.

 

♣ BMP 파일 생성

테스트를 위해 BMP 파일을 하나 만든다.

  • Visual Studio 2017 Express
  • Windows API 기반 C++
  • 콘솔 환경에서 컴파일하여 결과를 얻을 수 있다.
  • main() 함수에서 test.bmp 를 본인의 파일로 적절히 수정한다.
#include <cstdio>
#include <string>

#include <list>
#include <map>

#include <windows.h>

using namespace std ;

int compressData(list<string>* pList)
{
	list<string>::iterator pListIter ;	
	list<string>::iterator pListIter2 ;	

	pListIter2 = pList->end() ;
	int flagFirst = 1 ;

	// check data is same as above line
	if(pList->size() > 1)
	{
		while(1)
		{
			pListIter2-- ;
			pListIter = pListIter2 ;
			pListIter-- ;

			if(!pListIter->compare(*pListIter2))
				*pListIter2 = ":" ;

			if(pListIter == pList->begin())
				break ;
		}
	}

	// Check last data "0"
	size_t pos ;
	int p ;
	for(auto& zpl : *pList)
	{
		pos = zpl.find_last_not_of("0") ;

		// Skip if the last character is not "0"
		if(zpl.back() != '0')
			continue ;

		p = (pos == string::npos) ? -1 : pos ;
		zpl.replace(zpl.begin()+(p+1), zpl.end(), ",") ;
	}

	// Check last data "F"
	for(auto& zpl : *pList)
	{
		pos = zpl.find_last_not_of("F") ;

		// Skip if the last character is not "0"
		if(zpl.back() != 'F')
			continue ;

		p = (pos == string::npos) ? -1 : pos ;
		zpl.replace(zpl.begin()+(p+1), zpl.end(), "!") ;
	}

	// check ducplicated character.
	list<pair<char, int> > 			listCount ;
	list<pair<char, int> >::iterator 	listCountIter ;
	int len, cnt ;
	char prev, cur ;

	for(auto& zpl : *pList)
	{
		string strOrg = zpl ;
		len = zpl.length() ;
		prev = 0;
		cnt = 0 ;
		listCount.clear() ;
		for(int ii = 0; ii < len; ii++)
		{
			cur = zpl[ii] ;

			if(cur == prev)
			{
				cnt++ ;
			}
			else 	// cur != prev
			{
				if(prev)
					listCount.push_back(make_pair(prev, cnt)) ;
				cnt = 1 ;
				prev = cur ;
			}

			if(cnt == 400)
			{
				listCount.push_back(make_pair(cur, 400)) ;
				cnt = 0 ;
			}

			if(ii == len-1 && cnt > 0 )
				listCount.push_back(make_pair(cur, cnt)) ;

		}

//		printf("======= List Count ======\n") ;
		int quotient, remainder ;

		zpl.clear() ;
		char ch1, ch20;
		for(auto data : listCount)
		{
//			printf("%c - %d\n", data.first, data.second) ;

			if(data.second <= 2)
			{
				zpl += data.first ;
				if(data.second == 2)
					zpl += data.first ;
				continue ;
			}

			// 3 <= data.second <= 19
			if(data.second < 20)
			{
				ch1 = 'G' + data.second - 1 ;
				zpl += ch1 ;
				zpl += data.first ;
				continue ;
			}

			// 20 <= data.second <= 400
			quotient = (int)data.second/20 ;
			remainder = data.second % 20 ;

			ch1 = (remainder > 0) ? 'G' + remainder - 1 : '0' ;
			ch20 = 'g' + quotient - 1 ;

			zpl += ch20 ;
			if(remainder > 0)
				zpl += ch1 ;	

			zpl += data.first ;
		}

//		printf("ORG : %s\n", strOrg.c_str()) ;
//		printf("NEW : %s\n", zpl.c_str()) ;
	}

	return 1 ;
}

string genZPL_Bitmap(const char* szFile)
{
	BITMAPFILEHEADER bmFile ;
	BITMAPINFOHEADER bmInfo ;
	unsigned char colorTable[4]  ;

	int flagInvert=0 ;
	char szText[1024] ;

	string strZpl ;

	strZpl = "^XA" ;
	strZpl += "^FO20,20" ;

	sprintf(szText, "./%s", szFile) ;
	FILE* fp = fopen(szText, "rb") ;

	if(!fp)
	{
		printf("genBitmap File Error - %s\n", szText) ;
		strZpl.clear() ;
		return strZpl ;
	}

	fread(&bmFile, sizeof(BITMAPFILEHEADER), 1, fp) ;
	fread(&bmInfo, sizeof(BITMAPINFOHEADER), 1, fp) ;
	fread(colorTable, sizeof(colorTable), 1, fp) ;

	printf("File : %s\n", szFile) ;
	printf("bmFile.bfSize : %d\n", bmFile.bfSize) ;
	printf("bmFile.biWidth : %d\n", bmInfo.biWidth) ;
	printf("bmFile.biHeight: %d\n", bmInfo.biHeight) ;

	printf("ColorTable : %02X %02X %02X %02X\r\n",
		colorTable[0], colorTable[1], colorTable[2], colorTable[3]) ;

	if(colorTable[0] * colorTable[1] * colorTable[2] < 0x800000)
		flagInvert = 1 ;

	printf("FlagInvert = %d\r\n", flagInvert) ;

	fseek(fp, bmFile.bfOffBits, SEEK_SET) ;

	printf("bmFile.bfOffBits = %d (0x%02X)\r\n", bmFile.bfOffBits, bmFile.bfOffBits & 0xff) ;
	printf("bmInfo.Width = %d\r\n", bmInfo.biWidth) ;

	int width = (bmFile.bfSize - bmFile.bfOffBits)/bmInfo.biHeight ;
	int realWidthByte = (bmInfo.biWidth + 7) / 8 ;
	
	printf("Width ; %d\r\n", width) ;
	printf("Real Width Byte; %d\r\n", realWidthByte) ;

	int imageSize = realWidthByte * bmInfo.biHeight ;
	printf("GFA Image Count : %d\r\n", imageSize ) ;

	sprintf(szText, "^GFA,%d,%d,%d,", imageSize, imageSize, realWidthByte) ;
	strZpl += szText ;

	list<string> 		listRow ;
	list<string>::iterator 	listRowIter ;

	string strRow ;
	char szTemp[32] ;
	int nTemp ;
	unsigned char shValue ;

	for(int ii = 0; ii < bmInfo.biHeight; ii++)
	{
		strRow.clear() ;
		for(int jj = 1; jj <= width; jj++)
		{
			fread(szTemp, 1, 1, fp) ;

			if(jj > realWidthByte)
				continue ;

			if(jj == realWidthByte)
			{
				nTemp = bmInfo.biWidth & 0x07 ;
				shValue = 0xff ; // case of flagInvert == 1 72x72
				shValue = (nTemp == 1) ? 0x80 : shValue ;
				shValue = (nTemp == 2) ? 0xC0 : shValue ;
				shValue = (nTemp == 3) ? 0xE0 : shValue ;
				shValue = (nTemp == 4) ? 0xF0 : shValue ;
				shValue = (nTemp == 5) ? 0xF8 : shValue ;
				shValue = (nTemp == 6) ? 0xFC : shValue ;
				shValue = (nTemp == 7) ? 0xFE : shValue ;

				if(flagInvert)
					szTemp[0] |= (shValue ^ 0xff) ;
				else
					szTemp[0] &= shValue ;
			}

			if(flagInvert)
				szTemp[0] ^= 0xff ;

			szTemp[1] = '\0' ;
			sprintf(szText, "%02X", szTemp[0] & 0xff) ;
			strRow += szText ;
		}

		listRow.push_front(strRow) ;
	}
	fclose(fp) ;

#if 1
	// compress Data
	printf("********** Before compress data **********\n") ;
	for(auto& pData : listRow)
		printf("%s\n", pData.c_str()) ;

	compressData(&listRow) ;

	printf("********** Aftercompress data **********\n") ;
	for(auto& pData : listRow)
		printf("%s\n", pData.c_str()) ;
#endif

	// Add Raw Data
	listRowIter = listRow.begin() ;
	for(; listRowIter != listRow.end(); listRowIter++)
		strZpl += *listRowIter ;

	strZpl += "^FS";
	strZpl += "^XZ";

	return strZpl ;
}

int main()
{
	string strZpl ;

	const char* szFile = "./test.bmp" ;
	strZpl = genZPL_Bitmap(szFile) ;

	printf("ZPL : %s\n", strZpl.c_str()) ;

#if 1
	// copy ZPL to ClipBoard
	if(!OpenClipboard(NULL))
	{
		printf("Failed OpenClipboard()\r\n") ;
		return 1;
	}

	if(!EmptyClipboard())
	{
		printf("Failed EmptyClipboard()\r\n") ;
		return 1 ;
	}

	int len = strZpl.length() ;
	HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, len + 1) ;
	char* pClip = (char*)GlobalLock(hGlobal) ;
	strcpy(pClip, strZpl.c_str()) ;
	GlobalUnlock(hGlobal) ;

	if(!SetClipboardData(CF_TEXT, hGlobal))
	{
		printf("Failed SetClipboardData()\r\n") ;
	}

	CloseClipboard() ;
#endif

	return 1 ;
}

 

 

도움받은 사이트 : 

https://code.visualstudio.com/docs/remote/ssh

 

난 VirtualBox 로 Ubuntu Server 를 설치하고,

윈도우에서 putty 로 위 Ubuntu 에 SSH 로 접속하여 컴파일하는 것을 선호한다.

 

치명적인 단점은 Break Point 걸어서 디버깅하는 것이 Visual Studio 의 그것보다 못하다는 것이다.

 

뭐, Ubuntu Desktop 깔아서 여러 디버깅 툴(ddd 등) 을 깔아 써도 되지만

이것 때문에 가볍게 돌아가는 Ubuntu Server 를 포기하기엔 아까웠다.

 

윈도우에 MinGW 설치해 요래요래 하는 방법도 많던데, 뭔가 발전이 없어보여 안함.

( gcc 최신버전이 11.4 인데 MinGW 는 8.x 라든지... )

 

그래서 찾아보니, Visual Studio Code 에서 원격으로 접속해서 컴파일 및 디버깅까지

가능하다고 한다. 헐~

 

이에 아래처럼 정리한다.

(이 글 읽을 정도면 개발 초중급 이상은 되리라 보기에 VS code 설치 쪽에 중심을 두고 정리한다.)

 

1. Ubuntu Server 를 실행한다.

 

2. Visual Studio Code 를 설치한다.

 

3. VS Code 에서 Remote-SSH Extensions 을 설치한다.

 

4. VS Code 에서 F1 키를 눌러 아래창이 뜨면, "Remote-SSH : Connected to Host" 를 선택한다.

 

5. 아래처럼 Select configured SSH host or enter user@host 라고 뜨면,

아래 창에 접속할 id 와 호스트(IP) 를 입력한다.

나같은 경우, hahaite@192.168.56.2 이다. 

 

6. 그러면 아래처럼 새 창이 뜨면서 Enter password for hahaite@192.168.56.2 라고 뜬다. password 를 입력해 준다.

 

7. Ubuntu 에 VS Code Server 설치 (자동설치임)

위와 같이 password 를 입력하면 우측 하단에 VS Code server 인가를 세팅 중이라고 안내문구가 뜬다. (스샷 못찍음 ㅠㅠ )

 

설치가 완료되고 정상적으로 연결이 되면, 촤측 하단에 아래처럼 SSH : IP 가 뜬다.

 

이 때, Ubuntu 에서 ps -ef 명령어로 실행 중인 프로세스를 확인해 보면 아래처럼 vscode-server 라고 뭔가 떠있는게 보인다. 이는 윈도우의 vs code 에서 자동으로 설치해 준 것이다.

 

8. 설치

이제 File -> Open File 해보면 접속된 Ubuntu 의 디렉토리가 보인다.

아무 파일이나 열고 Run and Debug 아이콘을 클릭하고 Run and Debug 를 클릭한다.

 

 

아래처럼 뜨면 Install an extension for C++ 을 선택해 준다.

 

그리고 아래처럼 C/C++ IntelliSense, debugging 등을 설치해 준다.

아래는 최초 한번만 설치해 주는 것이며, Ubuntu Server 에 원격으로 설치되는 것으로 보인다.

 

9. 실행

이제 Run and Debug 를 다시 클릭하면 아래처럼 Ubuntu 의 컴파일러가 뜬다.

난 c++ 이니 두번째 g++ 을 선택했다.

 

그리고 VS Code 에서 아래 TERMINAL 을 선택하면 해당 코드가 실행되어 결과를 출력한 것을 확인할 수 있다.

 

10. Break Point

그리고 이 글을 쓰는 목적, Break point 테스트.

Visual Studio 의 단축키 (F9) 와 같게 Break Point 걸고 실행하면,

아래처럼 브레이크 걸려 각 변수값을 확인할 수 있으며,

F10, F11 로 진행할 수 있다. 우왕~

 

 

도움받은 사이트 :

https://leeeegun.tistory.com/3

https://stackoverflow.com/questions/6946217/how-to-access-the-contents-of-a-vector-from-a-pointer-to-the-vector-in-c

 

첫번째 링크 글에서 2차원 벡터 사용법을 쉽게 익힐 수 있었다.

 

그렇게 생성한 2차원 벡터를 포인터로 넘길 일이 있었는데 뜻대로 안되어 살펴보니

포인터로 넘겨진 2차원 벡터는 행렬기호 (대괄호) 를 사용할 수 없고 at() 함수를 통해 접근해야 했다.

(위 스택오버플로우 링크 참조)

 

만약 대괄호를 계속 사용하고 싶으면  레퍼런스(Reference)로 넘겨 사용해야 한다.

 

깔끔하게 레퍼런스로 넘기면 되겠지만,

선언과 동시에 할당해야 하는 레퍼런스 특성상 사용하기 까칠한 상황이 발생했다.

 

어쨌든, 아래처럼 두가지의 경우를 정리해 본다.

#include <cstdio>
#include <vector>

using namespace std ;

int callByPointer(vector<vector<int> >* pVecData)
{
	printf("============== Call by Pointer\n") ;
	printf("Size : [%ld][%ld]\n", pVecData->size(), pVecData->at(0).size()) ;

	for(int ii = 0; ii < pVecData->size(); ii++)
	{
		for(int jj = 0; jj < pVecData->at(0).size(); jj++)
			printf("%d ", pVecData->at(ii).at(jj)) ;
		printf("\n") ;
	}
	return 1 ;
}

int callByReference(vector<vector<int> >& pVecData)
{
	printf("============== Call by Reference\n") ;
	printf("Size : [%ld][%ld]\n", pVecData.size(), pVecData[0].size()) ;

	for(int ii = 0; ii < pVecData.size(); ii++)
	{
		for(int jj = 0; jj < pVecData[0].size(); jj++)
			printf("%d ", pVecData[ii][jj]) ;
		printf("\n") ;
	}
	return 1 ;
}

int main()
{
	vector<vector<int> > vecData ;
	vector<int> vecField ;

	vecData.clear() ;
	vecField.clear() ;

	vecField.push_back(2) ;
	vecField.push_back(3) ;
	vecField.push_back(4) ;
	vecData.push_back(vecField) ;

	vecField.clear() ;
	vecField.push_back(5) ;
	vecField.push_back(6) ;
	vecField.push_back(7) ;
	vecData.push_back(vecField) ;

	printf("==============\n") ;
	printf("size : [%ld][%ld]\n", vecData.size(), vecData[0].size()) ;
	for(int ii = 0; ii < vecData.size(); ii++)
	{
		for(int jj = 0; jj < vecData[0].size(); jj++)
			printf("%d ", vecData[ii][jj]) ;
		printf("\n") ;
	}

	// by Pointer
	callByPointer(&vecData) ;

	// by Reference
	callByReference(vecData) ;
	return 1 ;
}

 

main() 함수의 출력값, callByPointer(), callByReference() 함수에서

동일하게 값을 출력함을 확인하였다.

 

♣ 도움받은 사이트

docker-redmine : https://hub.docker.com/_/redmine

밍구의 코딩레시피 : https://minggu92.tistory.com/81

MeetUp (NHN) : https://meetup.nhncloud.com/posts/277

Docker 공식사이트 : https://docs.docker.com/compose/compose-file/compose-versioning/

Nirsa : https://nirsa.tistory.com/170 ( Docker 부팅 시 자동실행)

 

♣ 환경

 - Ubuntu Server 22

 - MariaDB Server 10.6

 

♣ 목적

Ubuntu Server 에는 이미 MariaDB 가 설치되어 있다.

Redmine 은 Docker 를 사용해 설치하고, DB 연동은 이미 설치된 MariaDB를 사용한다.

Redmine 은 docker-compose 를 사용하여 실행한다.

MariaDB 설치 및 docker 관련 세부 명령어는 이 글에서 다루지 않는다.

 

♣ docker 및 docker redmine 설치

docker, docker-compose 을 설치한다.

$ sudo apt install docker

$ sudo apt install docker-compose

$ sudo apt install docker-compose-v2

 

♣ MariaDB 에 Redmine 계정 생성

Redmine 자료가 저장될 MariaDB 에 Redmine 에서 사용할 계정을 만든다.

$ mysql -u root -p

 

로그인 되면,

1. 사용할 database 를 생성함. (아래 "redmine" 은 예제임. 수정 가능)

2. redmine DB 계정 및 패스워드 생성 (아래 "redmine" 계정 및 PW는 예제임. 수정가능)

3. 계정에 권한 부여

4. flush~

 

MariaDB>  create database redmine ;
MariaDB>  create user redmine@'%' identified by 'qwerty123@' ;
MariaDB>  grant all privileges on redmine.* to 'redmine'@'%' ;
MariaDB>  flush privileges ;

 

♣ docker-compose 작성

compose 가 저장될 디렉토리를 하나 만들어준다. 아래는 예시이다.

$ mkdir docker-redmine

$ cd docker-redmine

 

compose 파일을 작성한다. 파일 생성 후,

$ vi compose.yaml

그리고 아래처럼 작성해 준다.

 

아래 version 은 Compose File 버전이다. 이 값은 설치된 docker 버전을 따른다.

$ docker --version

 

설치된 docker version 은 24.0.5 이다.

이에 따른 compose 포맷 버전은 docker 공식 홈페이지에서 확인 가능하다.

https://docs.docker.com/compose/compose-file/compose-versioning/

 

 

위 글에 따르면 19.03 이상 버전은 3.8 을 따른다.

우리는 24.0 이니 3.8 이라고 썼다.

 

REDMINE_DB_MYSQL : DB 가 설치된 서버의 IP 나 hostname 을 적어준다.

Redmine 접속 시, http://192.168.0.143:8080 으로 접속한다면, 192.168.0.143 이 적혀야 한다.

나머지 항목은 직관적이므로 따로 설명하지 않으며

본인 환경에 맞게 적절히 수정하여 사용한다.

 

(아래는 복사용 )

version: '3.8'  # 도커 버전에 맞는 버전으로 작성

services:
   redmine:
      image: 'redmine'
      restart: always
      ports:
         - '80:3000'
         - '8080:3000'

      environment:
         TZ: Asia/Seoul
         REDMINE_DB_MYSQL: 192.168.0.143
         REDMINE_DB_PORT: 3306
         REDMINE_DB_DATABASE: redmine
         REDMINE_DB_USERNAME: redmine
         REDMINE_DB_PASSWORD: qwerty123@

      volumes:
        - '/home/hahaite/docker-redmine/data/files:/usr/src/redmine/files'
        - '/home/hahaite/docker-redmine/data/configuration.yml:/usr/src/redmine/config/configuration.yml'
        - '/home/hahaite/docker-redmine/data/themes:/usr/src/redmine/public/themes'
        - '/home/hahaite/docker-redmine/data/plugins:/usr/src/redmine/plugins'

#        - '/mesData/docker-redmine/data:/home/redmine/data'
#        - '/etc/localtime:/etc/localtime'

      container_name: 'redmine'

 

 

♣ docker-compose 실행

compose.yaml 파일이 있는 디렉토리에서,

$ sudo docker-compose up

이라고 명령을 치면 docker-redmine 이 실행된다.

 

주로 나오는 에러는 DB 에 접속할 수 없다는 내용인데, IP 및 DB 계정을 확인해 본다.

 

이상이 없으면 MariaDB 의 config 파일을 확인해 본다.

$ cd /etc/mysql/mariadb.conf.d

$ vi 50-server.cnf

 

위 파일을 열어서 bind-address 가 127.0.0.1 인지 확인한다.

만약 위 값이 세팅되어 있으면 주석처리한다.

 

그리고 mariadb 를 재실행하고,

$ service mariadb restart

 

docker-redmine 을 재실행한다.

 

별 에러없이 실행이 되면 아래처럼 접속되는지 확인한다.

 

본 절에선 심플하게 Redmine 을 설치하는 법을 다루었으며,

 

다음엔 보안을 적용해 https 접속을 다룰 예정이다.

♣ 도움받은 사이트

 - Development notes (https://whitekeyboard.tistory.com/730 )

 - 피어몬트, 그곳에 가면... (https://blog.startsomething.dev/2018/10/26/virtualbox-%EC%97%90%EC%84%9C-nat-%EC%99%80-host-only-network-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0/ )

 - WP Diaries ( https://www.wpdiaries.com/virtualbox-for-web-development/ )

 - 타쿠대디 ( https://takudaddy.tistory.com/352 )

 - 코덕 개발자노트 ( https://cjwoov.tistory.com/11 )

 

예전에 VirtualBox 의 브릿지와 NAT 개념 관련 글을 쓴 적이 있는데

그 때만 하더라도 자료가 많치 않았다. 2015년이니 9년 전이네... ㅋ~

Link : https://hahaite.tistory.com/283

 

최근에 관련하여 찾아보니 윗글보다 훨씬 자세하고 쉽게 쓴 글이 수두룩하다.

 Link : https://takudaddy.tistory.com/352

 

Virtual Box 네트워크 설정 정리

[목차] 1. 들어가며 2. 기본 용어 정리 3. 네트워크 방식 설명 4. 참고 [들어가며] 버추얼박스로 취약 서버를 구축해 테스트 및 실습을 해야 할 일이 많은데 가상 서버간의 네트워크 통신과 관련한

takudaddy.tistory.com

 

Link : https://cjwoov.tistory.com/11

 

[VM] VirtualBox 네트워크 설정 상세 분석

본 포스트는 https://technology.amis.nl/2018/07/27/virtualbox-networking-explained를 번역 & 가공하였습니다. 1. 서론 2. 네트워크 옵션 2.1 내부 네트워크 2.2 NAT 2.3 NAT 네트워크 2.4 호스트 전용 어댑터 2.5 브릿지 네

cjwoov.tistory.com

 

관련 개념은 위 두 링크를 참고바라며 아래는 위 개념을 어느정도 알고 있다고 가정하고 쓴다.

 * 호스트 : Windows 11

 * 게스트 : Ubuntu server 22

(한마디로 윈도우에 VirtualBox 설치했고, VirtualBox 사용해서 리눅스 돌리고 있다.)

 

♣ 어댑터에 브릿지

리눅스는 IPTime  과 같은 외부 공유기로부터 IP를 받는다.

제일 깔끔한 방법이나 공유기가 필요하다.

(그래서 출장갈 때 항상 공유기를 챙겼던 시절이 있었다. ㅋ)

 

♣ NAT

NAT 로 설정하면 리눅스는 10.0.2.15 의 IP를 할당받는다.

그리고 리눅스에 접속할 때 아래 192.168.56.1 로 리눅스에 접속할 수 있다.

 

그러나 포트포워딩 귀차니즘이 있고 게스트 간에 통신이 안되는 이슈가 있다.

 

♣ 호스트 전용 어댑터

호스트 전용 어댑터로 설정하면 아래처럼 192.168.56.xxx 로 IP 가 할당된다.

(이 IP는 192.168.56.1 가상 공유기로부터 DHCP 로 할당받는 것으로 알고 있다.)

그리고 호스트에서 이 IP 로 접속하면 된다. (NAT와 달리 포트포워딩 필요없음)

 

하지만 결정적으로 외부 접속 (인터넷 등)이 안된다. OTL

 

♣ NAT + 호스트 전용 어댑터 조합

표 출처 : 코덕 개발자 노트 ( https://cjwoov.tistory.com/11 )

 

VirtualBox 는 복수의 네트워크 어댑터 설정이 가능하다.

즉, 어댑터 1에는 NAT 로, 어댑터 2 에는 호스트 전용 어댑터로 설정하여 상호 단점을 보완하는 것이다.

 

이 후, 게스트 접속은 호스트 전용 어댑터 IP 로 (192.168.56.xxx ) 접속할 수 있으며

게스트의 외부망 접속은 NAT 를 통해 이루어지게 된다.

 

여기까지 하고 ifconfig 해보면 NAT 설정 IP (10.0.2.15) 만 나타나고

호스트 전용 어댑터 (192.168.56.xxx ) 는 안보인다.

 

그런데 ip addr 명령어로 보면 아래처럼 IP 할당이 안된 이더넷이 하나 보인다.

이 놈이 호스트 전용 어댑터의 이더넷임을 알 수 있다.

 

이에 (위의 경우) enp0s8 에 IP 를 할당해 준다.

 

본 글의 예제는 Ubuntu Server 22 환경이므로, 이 환경에 맞게 아래처럼 작업해 준다.

(당연한 얘기지만 각각의 게스트 OS 에 맞게 설정해 준다.)

 

아래는 호스트 전용 어댑터에 192.168.56.5 에 Subnet Mask 255.255.255.0 을 적용한 예이다.

$ cd /etc/netplan

$ sudo vi 00-installer-config.yaml  (파일명은 다를 수 있음)

그리고 아래처럼 호스트 전용 어댑터(enp0s8) 부분을 추가하였다.

그리고 아래 명령어 적용하거나 재부팅하여 ifconfig 해보면,

$ sudo netplan apply

$ ifconfig

 

아래처럼 호스트 전용 어댑터를 확인할 수 있다.

이제 게스트에서 아래 IP 192.168.56.5 로 접속하면 된다.

또한 NAT 어댑터를 통해 외부접속도 가능한 환경이 된다.

 

♣ 덧글

https://blog.startsomething.dev/2018/10/26/virtualbox-%EC%97%90%EC%84%9C-nat-%EC%99%80-host-only-network-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0/

 

위 글에 의하면 VirtualBox 전역도구에서

DHCP 사용 안함 및 "수동으로 어댑터 설정" 으로 설정하라고 하고 있다.

그런데 위 설정 시, DHCP 는 그냥 사용함으로 두어도 됐다.

혹시나 안되면 아래 설정 부분을 수정해 보자.

 

lsblk

♣ 도움받은 사이트

https://sacstory.tistory.com/entry/%ED%95%98%EB%93%9C-%EC%B6%94%EA%B0%80%EC%8B%9C

https://access.redhat.com/documentation/ko-kr/red_hat_enterprise_linux/8/html/security_hardening/encrypting-block-devices-using-luks_security-hardening

https://blog.tinned-software.net/automount-a-luks-encrypted-volume-on-system-start/

https://ko.linux-console.net/?p=10366

https://ko.linux-console.net/?p=439

https://ko.linux-console.net/?p=9102

 

갑님께서 생산용 서버의 Disk or File System 에

Encryption 을 적용해 달라고 요청을 했다.

 

처음 해보는 작업이라 여분의 서버에 1TB 짜리 HDD 하나 꽂고

우분투만 6번을 설치하고 수십번의 재부팅 끝에 아래처럼 정리해 본다.

 

♣ 목적

================================================

Ubuntu Server (22.04) 의 Data 영역을  LUKS 로 암호화한다.

윈도우에서 C에는 프로그램만, D에는 데이터를 저장하는데

D 드라이브만 암호화하는 것과 같다.

================================================

(Linux 초급 영역은 아닌지라 설명이 불친절할 수 있으며 나도 헷갈리는 부분의 용어, 개념 등이 틀릴 수 있다.)

 

♣ 파티션 전체에 LUKS 적용하기

이미 설치된 Ubuntu 전체에 적용할 수도 있으나 절차가 복잡하였다.

Ubuntu 설치 시, 아래처럼 파티션에 LUKS 를 적용하는 옵션이 있었다.

위와 같이 심플하게 전체 영역에 LUKS 를 적용할 수 있다.

이 후, 하위에 생성되는 볼륨은 자동으로 LUKS 가 적용된다.

 

♣ 특정 볼륨에 LUKS 적용하기

Ubuntu 설치 시, LUKS 를 적용하지 않는 케이스이다.

단, LVM 은 적용한다. (위 이미지에서 Set up this disk as an LVM group )

 

저 항목을 선택하면 자동으로 아래처럼 파티션이 설정된다.

보면 1TB 중 100GB 만 할당된다.

프로그램 등을 설치하면 이 100GB 영역에 설치될 것이다.

나는 Ubuntu 설치 후, 나머지 900GB 를 LUKS 로 암호화하고 Data 영역으로 사용할 예정이다.

 

♣ 볼륨 생성 후, LUKS 암호화 적용

Ubuntu 설치 후,

lsblk

하면 아래처럼 100GB 파티션만 표시된다.

이에 나머지 영역에 추가로 볼륨을 생성해 준다.

sudo lvcreate -l 100%FREE -n cryptdata-lv ubuntu-vg

아래처럼 ubuntu-vg 라는 Volume Group 에 cryptdata-lv 라는 Logical Volume 이 생성되었다.

이 볼륨을 LUKS 로 포멧시킨다.

LUKS 는 모두 cryptsetup 명령어로 수행된다. Ubuntu Server 22의 경우, 기본으로 설치되어 있다.

(luksFormat 에서 대문자 주의)

sudo cryptsetup luksFormat /dev/ubuntu-vg/cryptdata-lv

정말 Format할지 물어보는데 대문자로 "YES" 를 입력한다.

이 후, 본 암호화볼륨을 사용하기 위한 패스워드를 설정해준다. (까먹으면 ㅈ된다.)

 

♣ 암호화 볼륨에 접근하기 위한 매핑네임 생성

위 cryptdata-lv 볼륨은 LUKS로 암호화되어 있으므로 바로 사용할 수 없다.

이에 매핑네임을 생성해 준다.

매핑네임은 내가 붙인 이름이다. 본명 볼륨은 아니고, man 을 봐도 그냥 name 으로 되어 있다.

 

위에서 생성한 암호화된 볼륨 cryptdata-lv 에 mesdata 란 매핑네임을 생성해 주었다.

sudo cryptsetup luksOpen /dev/ubuntu-vg/cryptdata-lv mesdata

암호화 볼륨에 접근하기 위한 패스워드를 물어보며 위에서 설정한 패스워드를 입력한다.

이렇게 생성하면 /dev/mapper/mesdata 가 생성된다.

이놈을 원하는 file format 에 맞게 포멧해 준다. 난 xfs 로 포멧하였다.

sudo mkfs.xfs /dev/mapper/mesdata

lsblk 로 확인해 보면, 하위에 뭔가 생성된 것이 보인다.

즉, LUKS 매핑네임 mesdata 는 암호화된 볼륨에 접근하기 위한 통로로 여기면 얼추 맞을 듯 하다.

 

♣ 암호화 영역 mount

이제 위에서 생성한 암호화 영역을 사용하기 위해 특정 디렉토리로 마운트한다.

나는 임의로 루트에 mesData 란 디렉토리를 생성했다.

sudo mkdir /mesData

그리고 위 mesdata 매핑네임을 마운트한다.

sudo mount /dev/mapper/mesdata /mesData

이상없으면 아래처럼 매핑네임이 /mesData 디렉토리로 성공적으로 마운트됐음을 확인할 수 있다.

이제 마운트된 /mesData 에 데이터를 저장하면 데이터는 암호화되어 하드디스크에 저장된다.

누군가 악의를 품고 하드디스크를 훔쳐서 읽으려 해도 암호화되어 있으므로 읽을 수 없다.

 

이 상태에서 재부팅하면, 아래 상태로 돌아간다.

그러면 다시 매핑 네임 생성해주고 -> 비밀번호 입력하고 -> 마운트 후, 사용하면 된다.

 

♣ 부팅 시, 자동 매핑네임 생성 (패스워드 입력모드)

재부팅할 때마다 위와 같이 매번 매핑네임을 생성하는 것은 비효율적이다.

/etc/crypttab 파일을 수정하여 부팅 시, 매핑네임을 생성할 수 있다.

 

1. 우선, 암호화된 볼륨의 UUID 를 얻는다.

cryptdata-lv 에 본인이 생성한 볼륨명을 적어준다.

sudo cryptsetup luksUUID /dev/ubuntu-vg/cryptdata-lv

 

2. /etc/crypttab 파일을 수정한다.

아마도 위 절차를 거쳤으면 /etc/crypttab 파일이 있을 것이다.

여기에, 본인이 생성할 매핑네임, 위 1번에서 얻은 UUID, none, luks 를 적어준다.

아래는 예시이다. UUID= 부분에 주의한다.

mesdata UUID=6c2a7380-9214-4245-a946-29275f58e5d4 none luks

3. 위와 같이 저장하고 재부팅하면 부팅할 때 암호화 볼륨의 패스워드를 물어본다.

입력 후, lsblk 하면 매핑네임이 생성된 것을 확인할 수 있다.

 

4. 이 후, 디렉토리에 마운트하여 사용한다. (자동 마운트는 저 아래에서~ )

 

♣ 부팅 시, 자동 매핑네임 생성 (키 파일 등록모드)

위와 같이 부팅 시, 패스워드를 입력하는 것이 좀 더 강력한 보안이 될 수 있다.

그러나 주어진 환경이 여의치 않을 수 있다.

 

필자는 서버가 베트남 공장에 설치되어 원격으로 작업을 진행한다.

그런데 공장 사정으로 서버를 재부팅할 일이 생기면, 그 때마다 서버 관리자는

원격으로 접속하여 암호화 볼륨의 패스워드를 입력해야 한다.

만약 입력할 수 없는 상황이면, 최악의 경우 생산 라인이 스톱된다.

 

이에 패스워드 입력 없이 매핑네임을 생성하는 방법을 기술한다.

 

1. 키 생성

특정 양식이 있는 것이 아니라 아무 파일이나 된다.

다른 사이트를 살펴봐도 랜덤하게 키를 생성하는 것을 볼 수 있다.

아래는 4k 짜리 랜덤한 파일을 생성하는 명령어이다.

dd if=/dev/urandom of=./keyfile bs=1024 count=4

 

2. 파일을 /etc 밑으로 옮긴다. (꼭 etc 아니어도 상관 없다.)

sudo mv keyfile /etc

 

3. 해당 키를 암호화 블록에 등록한다. (패스워드 입력 필요)

sudo cryptsetup luksAddKey /dev/ubuntu-vg/cryptdata-lv /etc/keyfile

 

아래처럼 키 등록이 됐는지 체크할 수 있다.

sudo cryptsetup luksDump /dev/ubuntu-vg/cryptdata-lv

 

4. crypttab 파일에 키를 기재한다.

/etc/crypttab 파일의 none 위치에 키 위치를 적어준다.

5. 재부팅 후, lsblk 로 매핑네임이 생성됐는지 확인한다.

 

♣ 디렉토리에 자동 마운트

서브 주제이기에 간단히 다룬다. /etc/fstab 을 열어 아래처럼 추가해주고 재부팅하면

매핑네임 mesdata 가 디렉토리 mesData 에 마운트된다.

/dev/mapper/mesdata /mesData xfs defaults 0 0

 

♣ 데이터 저장위치 변경

이 후, 주요 데이터가 저장되는 위치를 /mesData 로 설정하여

데이터 저장 시 암호화를 적용한다.

 

 

 

 

♣ 도움받은 사이트

설치 관련 : https://www.jimmdenton.com/hp-raid-tools-ubuntu/
설치 관련 : https://downloads.linux.hpe.com/SDR/project/mcp/
키 관련 : https://downloads.linux.hpe.com/SDR/keys.html

 

HP 서버에서 HDD 를 관리하는 Smart Storage Administrator(SSA) 툴 설치법입니다.

 

♣ 1. HP PGP Public Key 설치

아래 명령어로 HPE PGP Public Key 를 설치합니다.

 

// sudo 로 비밀번호 한번 입력하기 위한 용도

$ sudo ls 

// 키 다운로드 및 추가하기

$ curl https://downloads.linux.hpe.com/SDR/hpPublicKey2048.pub | sudo apt-key add -
$ curl https://downloads.linux.hpe.com/SDR/hpPublicKey2048_key1.pub | sudo apt-key add -
$ curl https://downloads.linux.hpe.com/SDR/hpePublicKey2048_key1.pub | sudo apt-key add -

 

♣ 2. Source list 에 HPE Repository 추가

아래 focal 은 Ubuntu 버전의 닉네임(?) 입니다.

전 Ubuntu 20.04 (Focal Fossa) 이기에 focal 이라 썼으며 OS 버전에 맞게 수정해야 합니다.

 

$ sudo add-apt-repository 'deb http://downloads.linux.hpe.com/SDR/repo/mcp focal/current non-free'

 

그리고 패키지 업데이트를 진행합니다.

$ sudo apt-get update

 

추가한 Repository 에서 무사히ㅋ 업데이트가 진행됐습니다.

 

♣ 3. 패키지 설치, 진행

아래 명령어로 패키지를 설치합니다.

$ sudo apt-get install ssa ssacli ssaducli

 

아래와 같이 무사히 설치가 됐습니다.

 

♣ 4. 확인

설치 후, 아래 명령어로 HDD 상태를 확인해 봅니다.

$ sudo ssacli ctrl all show config

 

현재 1.2TB 짜리 하나 꾲혀 있으며 Raid 0 으로 세팅됨을 확인할 수 있었습니다.

이상, 설치 성공한 글을 마칩니다. ㅋ

 

프로그램에 파일을 불러들여 BLOB 타입에 Insert, Update 하는 것을

구현하긴 했으나 쿼리문 수정 등 귀찮은 바가 몇개 있었다.

 

용량이 작은 파일을 BLOB에 업데이트할 때 유용할 것으로 보인다.

 

1. 파일의 HEX 값을 구한다.

나같은 경우, 회사에서도 무료인 HxD Hex Editor 를 사용한다.

장점은 파일을 읽은 후, 복사, 붙여넣기 하면 Hex 값만 깔끔하게 가져온다.

 

파일을 읽은 후, 복사 붙여넣기 하면 HEX 값만 얻는다.

5E58410D0A5E4259322C322E305E46530D0A5E5345453A5548414E47554C2E4441545E46530D0A5E4357312C453A4B464F4E54332E464E545E434932365E46530D0A5E464F35302C35305E41314E2C34302C34305E4644C7D1B1DBC3E2B7C2C5D7BDBAC6AE5E46530D0A5E5051322C312C312C595E46530D0A5E585A0D0A

 

이 후, 얻은 HEX 값을 아래 쿼리문에 넣으면 된다.

나는 common_key 테이블 밑에 data 라는 BLOB 에 값을 넣었다.

(원래 데이터보다 짧게 기재했다. 실제는 훨씬 길 것이다.)

 

update common_key set data=UNHEX('5E58410D0A5E425932~~~') where no=10 ;

 

♣ 확인

값을 넣었으니 CRC32 값을 비교하여 올바르게 들어갔는지 확인한다.

Select hex(crc32(data)) from common_key where no=10 ;

 

 

그리고 실제 파일의 CRC32 를 구해본다.

 7-Zip 프로그램을 쓰면 우클릭 시, Checksum 값을 쉽게 얻을 수 있다.

위와 같이, 실제 파일과 DB 값 간에 CRC32값이 일치하였고,

원하는대로 DB에 파일을 업로드 됨을 확인하였다.

 

뭐, 각자 툴 있으면 그거 쓰면 되고, 가끔 귀차니즘 발동할 때 쓸 수 있겠다.

 

"프로그램 업그레이드가 안됩니다."

헉~!! 함 놀라주고 살펴보니 나도 잘되고 옆에도 잘되고

다른 공장 모두 업그레이드 이상 없다.

아무리 살펴봐도 모르겠어 결국 토요일에 안산 공장 방문.

20분만에 원인찾고 재부팅하니 모든게 해결.

생산라인의 노트북, 그 오랜시간동안 재부팅 한번 없었다.

생산라인은 언제나 스펙타클하게 내 상상을 초월했고

오늘도 아~주 짜릿하다.

안산까지 한시간 달려온게 아까워 어디갈까 하다가

안산역 옆에 다문화거리 탐방.

간만에 외국기운 물씬 받았다.

관광지보다는 외국인 노동자 장터같은 느낌.

잘만 다듬으면 관광지로도 손색이 없을 것 같다.

흥하라~!!

 

 

 

이 거리를 중심으로 양쪽으로 주거지까지 같이 있다. 그야말로 저잣거리

 

아~ 베트남 가고 싶다.

 

 

외국 나가서야 볼 수 있는 상호명인데 한국에서 보니 신기하다.

 

둘러본 간판 중 가장 대륙스러웠던...

 

두리안 7만원. ㅎㄷㄷ

 

'상사꽃 > 때로는' 카테고리의 다른 글

딸의 카톡  (0) 2018.03.04
김연아의 성화 점화를 반대했다.  (0) 2018.02.25
공단오거리  (0) 2018.01.03
청춘들의 민중가요 뒷풀이 노래  (0) 2017.12.22
억겁의 5년  (0) 2017.05.09

인터넷 찾아보면 코드간 변환은 많이 있으나 MultiByte Char 과 WideChar의 설명은 흔치 않는 듯 하다.

이에 정리해 본다.

 

♣ MultiByte Characters

char ch[32] ; 와 같이 배열이 있다 치고, 이 배열에 한글을 입력한다. 라고 심플하게 기억하자.

char[0] 에 한글을 입력할 수 없다.

이에 여러 char 예를들어 char[0] ~ char[2] 와 같이 여러 바이트를 사용해 한글을 저장한다.

이를 MultiByte Char 라고 표현한다.

대표적으로 ANSI (CP949), UTF-8 이 있겠다.

 

 

 

 

♣ Wide Characters

넓은 캐릭터 ㅋ. Wide Char 의 경우, 위와 같이 가변길이가 아니라 모두 동일한 길이를 갖는다.

변수 Type 은 w_char 또는 TCHAR 이 될 것이다.

(TCHAR은 환경에 따라 64비트가 아닐 수도 있으나 대부분의 개발환경이 64비트이므로 TCHAR 도 포함했다.)

 

예를 들면 아래와 같다.

 

 

♣ 인코딩 선택

위 코드값 생성은 메모장을 추천한다.

메모장에서 텍스트 입력 후, 다른이름으로 저장하면 아래 인코딩 방식이 나온다.

 

원하는 타입으로 저장 후, Hex 코드를 보면 된다.

참고로, 나는 윈도우에서 HxD 프로그램을 사용해 Hex 코드를 본다.

무료이고 회사에서도 쓸 수 있다.

 

 

♣ 인코딩 변환

Windows API 는 인코딩 변환에 두 함수를 제공한다.

 - WideCharToMultiByte ( )

 - MultiByteToWideChar ( )

 

위 개념을 이해했다면 이제 함수명을 보고 이게 어떤 의미인지 파악이 쉬울 것이며 아래처럼 정리할 수 있다.

 

UTF-8 -> Unicode : MultiByteToWideChar ( ) 함수 사용

ANSI -> Unicode : MultiByteToWideChar ( ) 함수 사용

 

Unicode -> UTF-8 : WideCharToMultiByte ( ) 함수 사용

Unicode -> ANSI : WideCharToMultiByte ( ) 함수 사용

 

여기서 의문점이 생긴다.

그렇다면 UTF-8 에서 ANSI 는 어떻게 변환하는가, 또 그 반대는 어떻게 변환하는가?

한번에는 변환이 안되고 아래와 같이 Unicode 를 한번 거쳐야 한다 (라고 알고 있다.)

 

UTF-8 -> Unicode -> ANSI : 

ANSI -> Unicode -> UTF-8

 

함수 호출은 MultiByteToWideChar ( ) -> WideCharToMultiByte ( ) 가 된다.

 

 

코드 변환하는 예제는 인터넷에서 찾으면 무궁무진하며

무작정 복붙하지 말고 위 개념을 익힌 후, 코드를 보면 바로 이해가 갈 것이다.

 

삽질은 나 하나로 충분하다. ㅋㅋ

 

'삽질미학 > C,C++' 카테고리의 다른 글

2차원 벡터를 포인터로 넘겨 사용하기  (0) 2024.02.13
함수 strtok() 의 대안, strsep()  (0) 2018.07.12
[C++] 삼각형 구조의 하향식 길찾기  (0) 2017.07.18
[C++] 조합 구하기  (0) 2017.07.13
[C++] 순열 구하기  (0) 2017.07.12

♣ 도움받은 사이트

https://8gwifi.org/docs/python-rsa.jsp

https://stackoverflow.com/questions/21327491/using-pycrypto-how-to-import-a-rsa-public-key-and-use-it-to-encrypt-a-string

 

♣  Python 에서 RSA 를 사용하기 위해 아래와 같이 pycryot를 설치하였다.

1. pycropto 설치 (Linux)

https://pypi.python.org/pypi/pycrypto 에서 모듈을 다운받는다.

아래와 같이 설치한다.

$ sudo python3 setup.py install

 

설치하면... time.clock() 함수가 없다고 뜬다.

찾아보니 Python 3.8 부터 위 함수가 폐기됐다고 한다. 아오~~

결국 Python 2.7 에서 설치하였다.

 

리눅스에서

$ python -> 2.7

$ python3 -> 3.8 로 실행되기에 python 으로 실행했다.

 

♣  Python 에서 Public Key, Private Key 생성하기

Public Key, Priavte 키 생성 (PEM 방식)

from Crypto.PublicKey import RSA

#Export RSA public/private KEY in PEM format
key = RSA.generate(2048)
privKey = key.exportKey('PEM')
pubKey = key.publickey().exportKey('PEM')

 

#save PEM key into the file
with open('private.pem', 'w') as file:
        file.write(privKey)
        file.write('\n')

 

with open('public.pem', 'w') as file:
        file.write(pubKey)
        file.write('\n')

 

테스트를 위해 plain.txt 파일을 만들고 아래와 같이 내용을 추가한다. (변경 가능0

Open SSL, Python Test

 

♣  OpenSSL 로 키 테스트

Python에서 생성한 키로  OpenSSL 에서 테스트해 본다.

 

1. Public key 로 암호화하기

 $ openssl rsautl -encrypt -inkey ./public.pem -pubin -in ./plain.txt -out encrypt.txt

 

2. Private Key 로 복호화하기

 $ openssl rsautl -decrypt -inkey private.pem -in encrypt.txt -out decPlain.txt

 

3. plain.txt 와 decPalin.txt 의 내용이 같은지 확인해 본다.

 

♣  Python 에서 Public Key 로 암호화 하기.

위에서 생성한 public key 로 아래와 같이 암호화 해 본다.

 

Encrypt Plain Text by public key

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5 as Cipher_PKCS1_v1_5

fPri = open('private.pem', 'rb')
fPub = open('public.pem', 'rb')

 

prikey = RSA.importKey(fPri.read())
pubKey = RSA.importKey(fPub.read())

 

#RSA Encryption Using Public Key
fp = open('plain.txt', 'rb')

cipherKey = Cipher_PKCS1_v1_5.new(pubKey)
cipherText = cipherKey.encrypt(fp.read())

 

fEnc = open('encrypt.txt', 'wb')
fEnc.write(cipherText)

 

# archive
#cipherText = pubKey.encrypt(fp.read(),32)
#cipherMsg = cipherText[0]


fPri.close()
fPub.close()
fp.close()
fEnc.close()

위에서 암호화 한 문장 (encrypt.txt) 을 openSSL 에서 복호화할 수 있다.

 

♣ Python 에서 Private Key 로 복호화 하기.

위에서 생성한 Private Key 로 아래와 같이 복호화 한다.

 

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5 as Cipher_PKCS1_v1_5

fPri = open('private.pem', 'rb')

prikey = RSA.importKey(fPri.read())

 

#RSA Encryption Using Public Key
fEnc = open('encrypt.txt', 'rb')

cipher = Cipher_PKCS1_v1_5.new(prikey)
cipherText = cipher.decrypt(fEnc.read(), None).decode()

 

fDec = open('decPlain.txt', 'wb')
fDec.write(cipherText)

 

fPri.close()
fEnc.close()
fDec.close()

저 위의 OpenSSL 에서 암호화 한 후,

Python 에서 위와 같이 복호화하면 동일하게 복호화 되었다.

도움받은 사이트 :

* 도커란 무엇인가?

https://dev-youngjun.tistory.com/2

* Ubuntu 18 에 Docker 설치하기

https://blog.cosmosfarm.com/archives/248/%EC%9A%B0%EB%B6%84%ED%88%AC-18-04-%EB%8F%84%EC%BB%A4-docker-%EC%84%A4%EC%B9%98-%EB%B0%A9%EB%B2%95/

* docker-compose 설치하기

https://sarc.io/index.php/cloud/1701-docker-compose-latest-version-install

 

 

팀 서버에 Redmine 설치하려다 결국 docker 란 놈까지 설치하게 되었다.

 

도커는... 예를 들어면 VMware 나 virtual box 비스무리한 개념으로 이해하면 될 듯 하다.

 

예를 들어,

VirtualBox 에 우분투를 설치하고 열심히 이것저것 설치해서 개발환경을 구축했다고 치자.

그런데 옆에 동료가 나중에 합류해서 또 개발환경을 구축하게 된다면,

내가 했던 삽질을 또 반복할 것이다.

이 때 내가 만든 VirtualBox 이미지만 동료에게 전달하면 더이상 구축할 필요가 없는 것이다.

 

docker 도 이런 비슷한 개념인데 자세한 건 위 사이트에 잘 정리되어 있다.

 

암튼,

Redmine 설치하는 거 은근히 까다롭다. 사전에 설치할 것도 몇개 있고 설정도 해야하고

버전도 맞춰줘야 한다. 열심히 따라 했는데... 안돌아간다. ㅆㅂ

 

그런데 다른 사람이 이미 docker 를 이용해 redmine 을 설치해서 인터넷에 올려놨다. (무식한 표현... ㅋ)

그럼 나는 그 docker-redmine 이미지를 받아 로딩만 하면 된다.

위에 예처럼 말이다.

Redmine 의 경우, sameersbn/redmine 이 유명한 듯 하다.

 

본 글에선 docker 설치를 기술한다.

 

Ubuntu 버전마다 설치 옵션이 살짝 다른 듯 하다.

아래는 Ubuntu 18 의 설치법이며 다른 버전은 인터넷에서 쉽게 찾을 수 있었다.

 

 Docker 설치 (Ubuntu 18)

$ sudo apt update
$ sudo apt install apt-transport-https ca-certificates curl software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
$ sudo apt update
$ apt-cache policy docker-ce

 

$ sudo apt install docker-ce

 

 

여러개의 docker 를 서로 연관지을 수 있고, docker 와 외부 프로그램간 연동도 가능한 듯 하다.

이를 위해서 docker-compose 를 설치해 준다.

 

Docker Compose 설치 

// 1. jq 설치 (아래 버전 확인용)

$ sudo apt-get install jq

 

// 2. docker 의 최신 버전 확인

$ curl --silent https://api.github.com/repos/docker/compose/releases/latest | jq .name -r

 

// 3. 아래 $(VERSION) 부분을 위에서 얻은 최신버전으로 교체 후 설치

$ sudo curl -L https://github.com/docker/compose/releases/download/${VERSION}/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose

 

// 4. 권한 수정

$ sudo chmod 755 /usr/local/bin/docker-compose

 

이제 docker-redmine 을 설치할 준비가 되었다.

 

docker-redmine 은 다음 글에 쓴다.

 

연구소에서 제조사업부로 소속을 옮기니 개발 관련된 건 다 자갈밭이었다.

 

연구소에 있을 땐 후임들이 알아서 삼바 설치하고 운용하고 했는데,

여긴 그 비싼 서버 갖다놓고도 쓰지도 않고 방치...

 

그래서 리눅스 서버에 삼바 설치하고 공용폴더 만들어줬더니

뻥 좀 붙여서 기립박수 받았다. ㅎㅎ

 

암튼, 백만년만에 삼바 설치하면서 나온 이슈 정리해 본다.

 

사용자 : user1, user2, user3

그룹명 : product

 

♣ 사용자 생성하기

$ sudo useradd -m -g product user1

  - m 옵션을 줘야 home 밑에 계정 디렉토리를 생성한다.

  - g 옵션을 주고 그룹을 할당한다. (이거 빼먹었으면 usermod 에서 추가할 수 있다.

 

♣ 신규 사용자의 passwd 설정

$ sudo passwd user1

 

♣ 위 모든 걸 한방에 할 수 있는게

$ sudo adduser user1

 - useradd 가 아니라, adduser 로 하면 이름 등등을 기입하라고 나온다.

 

♣ 사용자 확인

$ sudo vi /etc/passwd

 

♣ 그룹 확인

$ sudo vi /etc/group

 

♣ 기존 사용자를 기존 그룹에 추가하기

$ sudo usermod -a -G product user1

 

♣ 삼바에 신규 사용자 추가하기

$ sudo smbpasswd -a user1

 

♣ 삼바 설정

 - 사용자는 개인 디렉토리와 공용디렉토리 common 만 보인다.

 - common 에서 남이 작성한 파일은 다른 사람이 수정할 수 있어야 한다.

 - 제조팀 이외는 접근할 수 없다.

 

♣ 삼바 개인 설정

/etc/samba/smb.conf 파일을 열고 아래와 같이 팀원들을 추가함.

[user1]
    comment = user1 HOME
    path = /home/user1
    public = no
    writable = yes
    browseable = no
    valid users = user1

 

♣ 삼바 공용 공간 설정

/home/common 디렉토리를 공용 디렉토리로 만들고,

디렉토리의 group 권한을 product 로 둔다.

/etc/samba/smb.conf 에서 아래와 같이 작성하였다.

user1 이 올린 파일을 user2 가 수정할 수 없었는데 force group 으로 해결했다.

 

[common]
    comment = common HOME
    path = /home/common
    public = yes
    writable = yes
    directory mode = 0775
    create mode = 0775
    force group = product
    valid users = user1 user2 user3

 

추가사항

♣ 사용자 삭제

$ sudo userdel [user]  // 계정만 삭제

$ sudo userdel -r [user]  // home directory 포함하여 삭제 (그룹에서도 삭제되려나?)

 

 

출처 : https://www.lesstif.com/pages/viewpage.action?pageId=61899162

 

이래저래 찾은 방법 중에 이 방법이 제일 심플하고 바로 적용이 되었다.

 

전체 타임존 목록 보기

♣ 이 목록을 보고 바꾸려는 timezone 을 확인한다.

 $ timedatectl list-timezones

 

♣ 타임존 바꾸기 (ex) 한국)

 $ timedatectl set-timezone 'Asia/Seoul'

 

♣ GMT 로 설정하기

 $ timedatectl set-timezone 'GMT'

 

♣ 원하는 시간으로 변경되었는지 확인하기

 $ date

 

 

Linux 서버(Ubuntu Server)에 파일을 옮기거나 할 때 FTP를 사용해도 되지만

FTP 설치 등의 귀차니즘이 발동한다면......

 

Linux 서버의 IP : 192.168.15.25 라 하고...

(hahaite 는 제 계정입니다. 예시로 그냥 적음. ㅋ)

 

♣ SFTP 접속 후, 업로드하기

 $ sftp hahaite@192.168.15.25

password : (패스워드 입력)

SFTP > put test.tar

SFTP > quit

 

♣ SFTP 접속하면서로드하기

 $ sftp hahaite@192.168.15.25 <<< $'put test.tar'

 

♣ SSH Private Key 를 사용하여 접속하기

(ssh key 사용법은 본 글에서는 다루지 않는다.)

-I 옵션 뒤에 SSH Private Key 위치를 적어주면 password 없이 접속이 된다.

 $ sftp -i ~/.ssh/hahaite 192.168.15.25  (hahaite 는 제 SSH Private Key 임)

 

만약 private Key 가 ~/.ssh/id_rsa 로 되어 있으면 따로 안적어도 된다.

 $ sftp 192.168.15.25

(접속 끝)

 

 

♣ Batch File 을 이용한 업로드

SFTP 접속 시, Batch File 을 적어주면 접속 후, Batch File 안의 SFTP 명령어를 수행한다.

 

ex) mysftp.sh 란 파일을 만들고 아래 내용을 추가한다.

cd backup

put test.tar

 

그리고 -b 옵션에 위 파일을 기재해 준다.

 $ sftp -b ./mysftp.sh 192.168.15.25

 

♣ SFTP 자동 업로드 구성

매일 새벽 1시에 자동으로 DB 가 백업되고,

이 백업 파일을 다른 서버에 SFTP 로 옮긴다고 가정해 보자.

 

매일 새벽 1시가 되면 backup.sh 란 쉘스크립트가 자동 실행된다.

 

DB Backup 시, 날짜 + 시간이 기록되도록 한다.

(Root2020 은 root 계정의 MariaDB의 비밀번호임)

 

OPTIONS="-u root -pRoot2020 --routines --extended-insert --quick --single-transaction"

BACKUP_DIR=/home/hahaite/MDB_Backup

 

♣ SFTP를 이용한 Database Backup 수행

mysqldump $OPTIONS $DB_NAME > $BACKUP_DIR/DB_$(date "+%Y-%m-%d_%H").sql

# SFTP 에 사용할 Batch File 을 생성한다.

# 이 때, 위에서 언급한 SSH Key 를 사용해 자동 로그인되도록 한다. password 를 물으면 안되니까...

# 첫줄은 > 가 한개, 둘째줄은 > 가 두개 ( >> ) 임을 주의한다.

echo "cd dbTest" > mysftp.sh
echo "put ${DB_NAME}_$(date "+%Y-%m-%d_%H").sql" >> mysftp.sh
cat ./mysftp.sh

 

batch File 을 사용하여 업로드한다.

sftp -b ./mysftp.sh 192.168.15.25

=============================

아래는 내가 만든, 여러개의 DB 를 백업하는 쉘스크립트 예제이다.

 

#!/bin/sh

 

OPTIONS="-u root -pRootPassword --routines --extended-insert --quick --single-transaction"
BACKUP_DIR=/home/hahaite/MDB_Backup

DB_LIST="mydb_1"
DB_LIST="$DB_LIST mydb_2"
DB_LIST="$DB_LIST mydb_3"
DB_LIST="$DB_LIST mydb_4"

for DB_NAME in $DB_LIST
do
        mysqldump $OPTIONS $DB_NAME > $BACKUP_DIR/${DB_NAME}_$(date "+%Y-%m-%d_%H").sql
        echo "cd dbTest" > mysftp.sh
        echo "put ${DB_NAME}_$(date "+%Y-%m-%d_%H").sql" >> mysftp.sh
        cat ./mysftp.sh
        sftp -b ./mysftp.sh 192.168.15.25
done

 

♣ SFTP를 이용한 Database Backup 수행

위 스크립트는 내가 직접 실행하면 문제가 없지만 타이머에 의해서 실행되면 에러가 난다.

내가 직접 실행하면 내 계정으로 실행하는 것이지만,

타이머로 실행되면 root 계정으로 실행되기 때문이다.

 

이에 위 do ~ done 부분을 아래와 같이 수정하여 해결하였다.

-u 옵션에 내 계정(hahaite)를 지정해 주고, ssh private key 를 사용하여 백업서버에 접속하도록 하였다.

 

do
        mysqldump $OPTIONS $DB_NAME > $BACKUP_DIR/${DB_NAME}_$(date "+%Y-%m-%d_%H").sql
        echo "cd dbTest" > mysftp.sh
        echo "put ${DB_NAME}_$(date "+%Y-%m-%d_%H").sql" >> mysftp.sh
        cat ./mysftp.sh
        sudo -uhahaite sftp -i /home/hahaite/.ssh/hahaite -b $BACKUP_DIR/mysftp.sh 192.168.15.25

done

 

+ Recent posts