Skip to Content
IoT 앱 개발IronFlock CLISSH를 통한 원격 머신

SSH를 통한 원격 머신

--host는 ironflock 명령을 이 머신 대신 다른 머신에서 SSH로 실행합니다. 이 플래그를 사용하면 Mac을 포함한 노트북에서, 디바이스에 ironflock을 설치하지 않고 디바이스를 관리할 수 있습니다. 여러 번 지정하거나 호스트 파일과 함께 사용하면 여러 머신에서 한 번에 명령을 실행합니다.

ironflock --host [user@]machine <command> [flags] ironflock --host [user@]machine --host [user@]machine ... <command> [flags] ironflock --hosts <file> [--parallel <n>] <command> [flags]

completion을 제외한 모든 명령을 이 방법으로 다른 머신에서 실행할 수 있습니다. --host, --hosts, --parallel은 전역 플래그이므로 명령 이름의 앞이나 뒤에 올 수 있습니다.

머신 하나

--host [user@]machine은 같은 명령을 같은 플래그로 해당 머신에서 실행합니다. 명령이 출력하는 내용은 그대로 돌아옵니다. stdout은 stdout으로, stderr는 stderr로 돌아오며, 종료 코드도 그대로 돌아옵니다.

$ ironflock --host pi@device-17 version ironflock 2.0.2 linux/arm64 $ ironflock --host pi@device-17 agent remove --yes --json 2>/dev/null {"action":"removed","removed":[...]}

다른 머신의 요구 사항

  • amd64, arm64 또는 32비트 ARM(v5~v7)의 Linux. 다른 시스템에서는 종료 코드 3으로 끝납니다.
  • 명령을 실행하는 SSH 서버. scp나 SFTP는 필요하지 않습니다.
  • POSIX 로그인 셸: sh, bash, dash 또는 zsh. csh, tcsh, fish에서는 종료 코드 3으로 끝나며 그 사실을 메시지로 알려 줍니다.
  • root 로그인 또는 sudo: root가 필요한 명령에 필요합니다(다른 머신에서의 root 권한 참조).

머신에 ironflock이 설치되어 있을 필요는 없습니다. 실행할 때마다 자체 바이너리를 가져갑니다.

SSH 설정

--host는 이 머신의 ssh를 사용하므로, ~/.ssh/config, 키, SSH 에이전트, 점프 호스트가 ssh에서와 똑같이 동작합니다. 명령에 필요 없는 기능은 끕니다: 에이전트 및 X11 포워딩, 포트 포워딩, RemoteCommand, LocalCommand.

--host는 [user@]machine만 받습니다. 포트나 그 밖의 옵션은 ~/.ssh/config에 지정합니다:

Host device-* User pi Port 2222 StrictHostKeyChecking accept-new

이렇게 설정하면 ironflock --host device-17 status는 포트 2222에서 pi로 로그인하고, ssh는 아직 모르는 디바이스의 호스트 키를 받아들입니다. -로 시작하는 --host 값은 거부되며, 문자, 숫자, ., _, -, 그리고 IPv6 주소의 :와 % 이외의 문자가 들어 있는 값도 거부됩니다.

제어 소켓의 경로가 충분히 짧으면 SSH 연결 하나로 실행 전체를 처리합니다. 소켓은 임시 폴더 아래에, 그렇게 할 수 없으면 /tmp 아래에 만들어집니다. 이 경우 ssh는 비밀번호를 한 번만 묻습니다. 그렇지 않으면 실행의 각 단계가 자체 연결을 열고, ssh는 단계마다 다시 묻습니다.

ironflock agent install 또는 IronFlockOS가 설정한 디바이스에는 프로젝트 소유자용 SSH 사용자가 있으며, 이 사용자는 디바이스 .flock 파일의 키로 로그인합니다. 사용자 이름과 파일에서 키를 꺼내는 방법은 SSH 접근에 나와 있습니다.

머신에서 일어나는 일

  1. 바이너리. 머신의 플랫폼이 같으면 여기서 실행 중인 바이너리를 받습니다. 그렇지 않으면 ironflock이 머신 플랫폼용 바이너리를 <download base>/cli/<os>/<arch>/<version>/ironflock에서 다운로드하고, 사용하기 전에 여기서 확인합니다. 이 바이너리에 들어 있는 IronFlock 릴리스 키로 서명(.asc)을 확인하고, 빌드 정보에 프로그램 ironflock, 머신의 플랫폼, 이 바이너리와 같은 소스 리비전이 기록되어 있는지 확인합니다. 다운로드한 파일은 캐시 폴더(Linux에서는 ~/.cache/ironflock, Mac에서는 ~/Library/Caches/ironflock)에 보관되며 사용할 때마다 다시 확인합니다.
  2. 폴더. 바이너리는 로그인 계정만 읽을 수 있는 새 폴더(mktemp -d)에 들어갑니다. 이 폴더는 머신의 ${TMPDIR:-/tmp} 아래에 만들어지며, 임시 폴더에서 프로그램을 실행할 수 없으면(noexec로 마운트된 파일 시스템) 로그인 계정의 홈 아래에 만들어집니다. 두 곳 모두에서 실행할 수 없으면 명령은 종료 코드 3으로 끝나며 두 곳을 모두 알려 줍니다. 폴더에서 무엇이든 실행하기 전에, 머신은 폴더와 바이너리가 로그인 계정 소유이고 심볼릭 링크가 아닌지, 그리고 바이너리가 예상한 버전과 플랫폼인지 확인합니다.
  3. 파일. 명령의 플래그에 지정된 파일이 함께 전달됩니다(플래그에 지정된 파일 참조).
  4. 명령. 로그인 계정으로, 또는 sudo를 통해 실행됩니다(다른 머신에서의 root 권한 참조).
  5. 정리. 명령이 끝나면 머신은 바이너리와 복사본이 든 폴더를 제거합니다. 실패한 뒤, 연결이 끊긴 뒤, 그리고 HUP, INT, QUIT, TERM, PIPE, USR1, USR2 시그널을 받은 뒤에도 제거합니다.

--host는 agent install 뒤에도 머신에 ironflock을 남기지 않습니다. 이후에도 --host로 계속 작업하거나, 인자 없이 실행하면 설치만 하는 설치 명령줄로 머신에 ironflock을 설치하십시오. --host를 통한 self-update는 실행의 바이너리가 아니라 그 머신의 /usr/local/bin/ironflock을 업데이트합니다(ironflock self-update).

다음 두 경우에는 ironflock.* 폴더가 머신에 남습니다:

  • 명령이 실행되는 동안 머신의 전원이 끊기거나 머신에서 kill -9가 실행된 경우. 폴더에는 바이너리와 복사본이 들어 있습니다.
  • 복사가 끝난 뒤 명령이 시작되기 전에 연결이 끊긴 경우, 또는 명령을 시작하는 단계가 연결하는 동안 여기서 실행이 중단된 경우. 폴더에는 바이너리가 들어 있으며, 터미널에서 명령을 실행했다면 복사본도 들어 있습니다.

.flock 파일의 복사본에는 디바이스의 시크릿이, 암호 문구 파일의 복사본에는 WiFi 암호 문구가, 비밀번호 파일의 복사본에는 프록시 비밀번호가 들어 있습니다. 로그인 계정과 root만 읽을 수 있습니다. 이런 폴더는 직접 제거하십시오.

플래그에 지정된 파일

플래그명령
-c, --config (.flock 파일)agent install, agent configure, doctor, os flash, os install, os reset
--wifi-password-fileos flash
--proxy-password-fileagent install, agent configure, os install

각 파일은 여기서 읽으며, 상대 경로는 이 머신의 작업 폴더를 기준으로 합니다. 암호 문구 파일이나 비밀번호 파일은 파이프(/dev/stdin)여도 되며, 한 번만 읽습니다. 내용은 SSH 연결의 표준 입력으로 머신에 전달됩니다. 파일도 그 크기도 여기서든 머신에서든 명령줄에 나타나지 않습니다. 머신에서는 모든 사용자가 명령줄을 읽을 수 있기 때문입니다. 머신은 각 크기를 명령이 받는 최대 크기와 비교한 다음, 전체가 도착했는지 확인합니다.

머신에서 복사본은 <folder>/1/device.flock입니다. 실행 폴더 안의 별도 폴더(모드 0700)에, 여기서와 같은 이름과 모드 0600으로 저장됩니다. 그곳의 명령은 이 경로를 받으므로(예: --config=/tmp/ironflock.k3Xf9QpL2m/1/device.flock), 파일 내용에 관한 오류는 복사본을 가리킵니다.

다음 경우에는 명령이 이 머신에서 끝납니다:

  • 종료 코드 1 — 일반 파일이 아닌 파일(암호 문구 파일이나 비밀번호 파일은 파이프여도 됨), 또는 명령이 받는 크기보다 큰 파일: .flock 파일은 5 MiB, 암호 문구 파일이나 비밀번호 파일은 4096바이트.
  • 종료 코드 2 — 제어 문자가 들어 있는 파일 이름.
  • 종료 코드 2 — os flash의 --image-file과 --output. --host는 이미지를 머신에 복사하지 않으며, 파일을 가져오지도 않습니다. 이 명령은 --host 없이 여기서 실행하거나 머신에서 직접 실행하십시오. --image-file이 없으면 머신이 직접 이미지를 다운로드합니다.

다른 머신에서의 root 권한

  • root가 필요한 명령은 로그인 계정이 root가 아니면 sudo를 통해 실행됩니다. 비밀번호와 터미널 없이 사용할 수 있는 sudo는 어디서나 동작합니다.
  • version, os images, os drives는 로그인 계정으로 실행됩니다.
  • status와 doctor는 root로 실행하면 더 많이 읽습니다. 로그인 계정이 root이거나 비밀번호와 터미널 없이 sudo를 사용할 수 있으면 root로 실행됩니다. 그렇지 않으면 묻지 않고 로그인 계정으로 실행되며, 힌트에 --host root@machine을 사용한 명령이 나옵니다.
  • agent logs는 root의 파일을 읽으므로 sudo를 통해 실행됩니다.
  • sudo가 비밀번호를 묻거나 설정에서 터미널을 요구하면(requiretty) 머신에 터미널이 필요합니다. 그러려면 여기서 stdin, stdout, stderr 모두에 터미널이 있어야 합니다. 터미널이 없으면 명령은 아무것도 복사하기 전에 종료 코드 3으로 끝납니다. 이 경우 root로 로그인하거나, 로그인 계정이 비밀번호와 터미널 없이 sudo를 사용할 수 있게 하거나, 리디렉션 없이 터미널에서 명령을 실행하십시오.
  • 명령을 실행하려는 시점에 sudo가 거부해도 종료 코드 3으로 끝납니다.

그곳의 명령이 다음에 실행하라고 안내하는 명령은 sudo 없이 이쪽에서 실행하는 명령입니다. --host가 직접 root 권한을 얻기 때문입니다: ironflock --host pi@device-17 agent start.

터미널, 질문, 출력

  • 여기에 터미널이 있으면 명령은 머신에서도 터미널을 받으므로 질문할 수 있습니다. 이때 stdout과 stderr는 합쳐져 사용자의 터미널로 돌아옵니다.
  • 여기에 터미널이 없으면(파이프, 프로그램으로 보내는 --json, CI) stdout과 stderr는 분리된 채로 유지되며, 명령은 아무것도 묻지 않습니다. 확인은 --yes로 합니다. 머신에서 로그인 계정의 파일이 출력하는 내용(예: echo하는 .bashrc)은 stdout에서 버려지므로, stdout에는 명령의 결과만 담깁니다.
  • --yes, --json, 명령의 플래그는 함께 전달됩니다. 다운로드 베이스는 --download-base로 전달되며, 그 값은 플래그 값 또는 이 머신의 IRONFLOCK_DL_BASE입니다. 로그인 정보가 들어 있는 다운로드 베이스(https://user:[email protected]/dl)는 어떤 머신에도 접속하기 전에 종료 코드 2로 끝납니다. 머신에서는 그곳의 모든 사용자가 읽을 수 있는 명령줄에 나타나기 때문입니다.
  • --help, help, 명령 없는 그룹, completion은 여기서 응답합니다. 출력 내용이 어느 머신에서나 같기 때문입니다.
  • 명령이 실행되기 전에 머신이 stdout으로 보내는 내용(머신의 응답과 로그인 계정 파일의 텍스트)은 64 KiB까지 읽습니다. 그보다 많으면 그곳에서 명령이 실행되기 전에 실행이 종료 코드 1로 끝납니다.

Ctrl-C와 연결 끊김

여기서 누른 Ctrl-C는 그곳의 명령에 자기 터미널에서 누른 Ctrl-C처럼 전달되고, 연결 끊김은 행업으로 전달됩니다. 여기에 터미널이 있든 없든 마찬가지입니다. Ctrl-C 후 --host는 머신이 그곳의 명령이 끝났다고 종료 코드(Ctrl-C로 끝났으면 130)와 함께 알려 줄 때까지 최대 15초 기다립니다. 머신이 제때 알려 주지 않거나 연결이 먼저 끊기면 --host는 130으로 끝나며, 명령이 그곳에서 아직 실행 중일 수 있다고 알려 줍니다. Ctrl-C가 왔을 때 명령이 아직 시작되지 않았던 실행은 명령을 시작하지 않습니다.

--host를 통한 os install에는 별도의 규칙이 있습니다. 질문에 답한 뒤에는 연결이 끊겨도 더 이상 멈추지 않으며, 명령은 머신이 IronFlockOS로 다시 돌아올 때까지 여기서 기다립니다. ironflock os install을 참조하십시오.

종료 코드와 오류

종료 코드는 명령의 종료 코드입니다(종료 코드). --host는 다음을 추가합니다:

코드의미
255ssh 실패: 연결 안 됨, 로그인 또는 호스트 키 거부, 연결 끊김
3머신이 명령을 실행할 수 없음: 다른 시스템이나 CPU, POSIX 셸이 아닌 로그인 셸, 프로그램을 실행할 수 있는 폴더 없음, 로그인 계정을 허용하지 않는 sudo
130인터럽트: 여기서(Ctrl-C), 또는 머신에서 명령에 대해(그곳의 행업, 인터럽트 또는 종료 시그널)

--host 자체의 실패도 다른 오류와 같습니다. stderr의 Error: 줄로, 또는 --json을 사용하면 stdout의 오류 객체로 나옵니다. 유일한 예외: 명령의 출력이 이미 전달된 뒤라면 그 출력이 결과이며, 오류는 --json을 사용해도 stderr의 Error: 줄로만 나옵니다.

여러 머신

--host를 여러 번 지정하거나 --hosts <file>을 사용하면, --host가 머신 하나에서 실행하듯이 각 머신에서 명령을 실행합니다. --host의 머신이 먼저 오고, 그다음 파일의 머신이 옵니다. 머신이 하나만 있는 호스트 파일도 여러 머신일 때와 같은 방식으로 실행됩니다.

$ ironflock --host pi@vm1 --host pi@vm2 --host pi@vm3 status pi@vm2: Agent: installed in /opt/ironflock/device-agent pi@vm2: Version: 1.6.9 pi@vm2: Service: running (ironflock-device-agent.service) pi@vm3: ssh: connect to host vm3 port 22: Connection timed out pi@vm3: Error: the SSH connection to pi@vm3 failed pi@vm1: Agent: installed in /opt/ironflock/device-agent ... Summary of "status" on 3 machines, exit code 255: 2 done, 1 ssh. pi@vm1 done pi@vm2 done pi@vm3 255 ssh: the SSH connection to pi@vm3 failed (ssh: connect to host vm3 port 22: Connection timed out)

호스트 파일

파일에는 한 줄에 [user@]machine을 하나씩 적습니다. #은 줄 끝까지 이어지는 주석을 시작하며, 빈 줄은 무시됩니다. 파일은 --hosts <(...) 같은 파이프여도 되며, 최대 1 MiB입니다.

# 포장 라인 3 pi@device-17 pi@device-18 pi@device-19 # the spare unit
# 파일에 있는 모든 머신의 상태를 20대씩 동시에 표시 ironflock --hosts line3.txt --parallel 20 status # 파일에 있는 모든 머신의 에이전트 업데이트. agent update는 질문하므로 --yes가 필요 ironflock --hosts line3.txt agent update --yes # 한 사이트의 디바이스에 프록시 계정 하나를 지정. 비밀번호는 파일에서 읽음 ironflock --hosts site-a.txt agent configure --https-proxy http://[email protected]:3128 --proxy-password-file proxy-password.txt --yes

다음 경우에는 아무것도 실행하기 전에 종료 코드 2로 끝나며, 오류 메시지에 파일과 줄이 나옵니다:

  • 단어가 둘 이상인 줄
  • --host가 거부할 머신
  • 두 번 지정된 머신. 같은 머신을 두 사용자로 지정한 것도 두 번으로 칩니다. 명령이 그 머신에서 동시에 두 번 실행되기 때문입니다. 한 머신의 주소와 이름은 같은 머신으로 인식되지 않습니다.

병렬 실행

최대 --parallel 대의 머신에서 동시에 실행합니다. 플래그가 없으면 10대이며, 1~100 사이로 지정합니다. 머신은 지정된 순서대로 시작하며, 하나가 끝나면 다음 머신이 시작합니다. 머신이 여러 대가 아닌데 --parallel을 지정하면 종료 코드 2로 끝납니다.

질문 없는 SSH

어떤 머신도 터미널을 받지 않으며, ssh는 아무것도 묻지 않습니다(BatchMode=yes). 다음 경우는 해당 머신에서 종료 코드 255와 ssh 자체의 메시지로 실패하고, 다른 머신은 계속 실행됩니다:

  • 비밀번호를 요구하는 로그인
  • 암호 문구가 SSH 에이전트에 없는 키
  • ssh가 아직 모르는 호스트 키

새 호스트 키를 받아들이려면 ssh로 한 번 접속하거나, 위의 예처럼 ~/.ssh/config에서 해당 머신에 StrictHostKeyChecking accept-new를 설정하십시오. ssh는 30초 후 접속을 포기하므로(~/.ssh/config의 설정과 관계없이 ConnectTimeout=30), 꺼져 있는 머신은 그때 보고됩니다. 그곳의 sudo가 비밀번호나 터미널을 요구하면 해당 머신에서 종료 코드 3으로 끝납니다. 이때 status와 doctor는 로그인 계정으로 실행됩니다.

여러 머신의 출력

  • 머신이 쓰는 각 줄은 앞에 <host>: 가 붙어 온전한 줄로 나옵니다. stdout은 stdout으로, stderr는 stderr로 나옵니다. 머신들의 줄은 도착하는 순서대로 섞입니다.
  • 줄바꿈 없는 마지막 줄에는 줄바꿈이 붙습니다. 64 KiB보다 긴 줄은 64 KiB씩 나뉘어 나오며, 각 부분 앞에 호스트가 붙습니다.
  • 탭 이외의 제어 문자는 줄과 요약 모두에서 이스케이프되어(\x1b, \r) 나오므로, 어떤 머신도 커서를 옮기거나 다른 머신의 줄을 덮어쓸 수 없습니다. 줄 끝의 캐리지 리턴은 버려집니다.
  • 여기서 실패한 실행은 --host가 머신 하나에 대해 쓰는 것처럼, 앞에 호스트가 붙은 Error: 줄을 추가합니다.
  • 마지막에 stderr에 요약이 나옵니다. 종료 코드와 각 결과로 끝난 머신 수가 나오고, 그다음 지정된 순서대로 머신마다 한 줄이 나옵니다. 각 줄에는 done, 또는 종료 코드와 그 종류, 이유가 나옵니다. 이유는 여기서 난 오류(255이면 ssh 자체의 메시지) 또는 명령의 Error: 줄입니다. Ctrl-C 후 명령이 끝났음을 확인해 주지 않은 머신은 interrupted, may still run입니다.

여러 머신의 종료 코드

명령은 머신들의 종료 코드 중 가장 나쁜 것으로 끝납니다. 모든 머신이 0으로 끝나면 0입니다. 그 밖에는 가장 나쁜 것부터 다음 순서입니다:

  1. 130 — 여기서 또는 머신에서 중단됨.
  2. 1 — 머신에서 명령이 실패함. 그곳에서 어떤 코드로 끝났든 머신이 절반만 변경된 상태로 남았을 수 있습니다.
  3. 255 — 머신에 접속하지 못했거나 연결이 끊김.
  4. 3 — 머신이 명령에 필요한 상태가 아니거나 명령을 실행할 수 없음.
  5. 4 — 질문이 거절됨.

종료 코드 2는 모든 명령에서와 마찬가지로 여기서의 잘못된 명령줄을 뜻합니다.

JSON 줄

--json을 사용하면 stdout에는 각 머신의 명령이 끝날 때 머신마다 한 줄이 끝나는 순서대로 나오며, 그 밖에는 아무것도 나오지 않습니다. 프로그램은 줄이 도착하는 대로 읽습니다.

{"host":"pi@vm1","state":"done","exit_code":0,"result":{...}}
필드의미
host--host 또는 호스트 파일에 지정된 그대로의 머신
statedone: 명령이 0으로 끝남. failed: 그곳에서 다른 코드로 끝났거나 여기서 실행이 실패함. interrupted: 실행이 중단되었고, 머신이 명령이 끝났다고 알려 줌. may_still_run: 여기서 실행이 중단되었고, 머신이 대기 시간 안에 명령이 끝났다고 알려 주지 않음. not_started: 머신의 차례가 오기 전에 실행이 중단됨. 그곳에서는 아무것도 실행되지 않음.
exit_code머신에서 명령이 끝난 방식, 또는 여기서 실행이 끝난 방식: 머신에 접속하지 못했거나 연결이 끊기면 255, 머신이 명령을 실행할 수 없으면 3, 실행이 중단되면 130. kill 후의 137 같은 다른 코드는 머신의 코드 그대로이며, 상태는 failed입니다.
result명령의 페이지에 설명된 대로 명령이 --json으로 쓴 내용. agent logs는 로그 줄의 배열이며, 최대 16 MiB까지 보관됩니다. 결과를 쓰고 1로 끝나는 명령(doctor, agent update)에도 이 필드가 있습니다.
error명령이 0으로 끝나지 않은 이유로, kind와 message가 있습니다: 여기서 난 실행 오류(접속하지 못한 머신은 종류 ssh이고, 메시지는 괄호 안의 ssh 메시지로 끝남), 없으면 명령의 오류 객체, 그것도 없으면 종료 코드와 stderr의 마지막 줄로 만든 오류
output명령이 stdout에 쓴 내용 중 JSON이 아닌 것. 또는 16 MiB보다 많이 썼으면 그 사실을 알리는 문장

stderr에는 머신들의 줄과 요약이 나옵니다. 머신들의 줄에는 --json에서도 자체 Error: 줄을 쓰는 명령(agent update)의 그 줄도 포함됩니다. 여기서 난 실행 오류는 stdout의 줄에만 나옵니다.

# status가 0으로 끝나지 않은 머신 ironflock --hosts line3.txt status --json 2>/dev/null | jq -r 'select(.state != "done") | .host'

여러 머신에서의 Ctrl-C

Ctrl-C, SIGTERM, SIGHUP은 머신 하나에서의 실행을 끝낼 때와 같은 방식으로 실행들을 끝냅니다. 그곳의 각 명령이 시그널을 받고, 각 머신은 15초 안에 명령이 끝났음을 확인해야 합니다. 확인하지 않은 머신은 may_still_run입니다. 차례가 오지 않은 머신은 아무것도 실행하지 않습니다. 요약에는 이 머신들이 not started로 나오며, 명령은 130으로 끝납니다.

여러 머신에서 실행되는 명령

명령여러 머신에서
version, status, doctor, os images, os drives, agent restart, agent start항상
agent logs항상. 단, --follow는 --json과 함께 쓸 수 없습니다. 머신의 줄은 명령이 끝날 때 나오는데 --follow는 끝나지 않기 때문입니다
agent stop, agent update, agent configure, agent remove, os update, os rollback, self-update--yes가 있을 때: 이 명령들은 질문하며, 여러 머신에서 동시에 실행하면 아무에게도 묻지 않습니다
completion--help처럼 여기서 응답하며, 어떤 머신에서도 실행되지 않음
agent install, os install, os flash, os reset불가: .flock 파일이 디바이스 하나를 지정하거나, 머신 하나를 지정하는 질문 뒤에 디스크, 드라이브 또는 디바이스의 데이터를 지웁니다

이 머신의 파일을 지정하는 플래그는 여러 머신에서는 거부됩니다. 그 파일이 모든 머신으로 전달되기 때문입니다. doctor와 agent configure의 -c는 디바이스 하나를 지정합니다. 예외는 agent configure의 --proxy-password-file로, 한 사이트의 머신들에 프록시 계정 하나를 지정하는 데 사용합니다.

여러 머신에서 실행되지 않는 명령줄은 호스트 파일을 읽기 전, 그리고 어떤 머신에도 접속하기 전에 종료 코드 2로 끝나며 그 이유를 알려 줍니다.

Last updated on