태그 보관물: Linux

Update failed: upgrade-temp-backup 디렉토리를 생성할 수 없습니다.

WordPress에서 몇 번 플러그인 업데이트 실패에 대한 알람이 왔었는데, 오늘에야 살펴보니 자동은 물론 수동 업데이트도 계속해서 실패하고 있었다.

새로 릴리즈된 WordPress 6.3에서 플러그인과 테마를 롤백할 수 있는 기능이 추가되었는데, 이 기능을 위해 wp-content/upgrade-temp-backup/plugins와 wp-content/upgrade-temp-backup/themes 디렉토리를 사용하는 모양이다. (관련 기사 링크)

문제의 원인은 간단했는데, wp-content 디렉토리의 쓰기 권한이 daemon에게 없기 때문에 디렉토리를 만들지 못하고 실패하는 것이었다. 따라서 간단히 디렉토리를 수동으로 만들고 권한을 할당해 주는 것으로 해결되었다.

콘솔에 접근해서 다음과 같이 명령어들을 입력한다.

# WordPress경로로 이동해서
#e.g) cd ~/apps/wordpress/htdocs

# 디렉토리 경로를 만들어주고
mkdir -p ./wp-content/upgrade-temp-backup/plugins
mkdir -p ./wp-content/upgrade-temp-backup/themes

# 소유권 그룹을 할당한 다음
sudo chown bitnami:daemon -R ./wp-content/upgrade-temp-backup

# daemon group에 쓰기 권한을 준다.
chmod 775 -R ./wp-content/upgrade-temp-backup

관리자 화면으로 돌아와서 설치 재시도 하니 잘 동작되었다.

Multi-container app의 Bad Gateway(502) 문제 디버깅

Intel Training and Learning Suite(TLS)는 처음으로 경험해보는 multi-container application이다. 중간에 native개발을 위한 환경 설정을 끼워 넣어 보려고 이것 저것 시도하는 중에 web interface를 담당하는 tls_proxy라는 docker가 Bad Gateway를 띄우면서 문제가 생겼다. 디버깅 하는 과정에서 사용해 본 docker-compose명령어 들의 쓰임새가 유용할 것 같아서 기록으로 남겨 둔다.

Port number와 docker찾기: docker-compose ps

Web browser로 서비스에 접근하면(https://localhost) Bad Gateway(502) error가 발생하는데 처음으로 web-browser의 접속 요청을 받아서 처리하는 docker container는 docker-compose ps 명령어로 docker image별로 listening하는 port number들을 찾을 수 있다.

TLS의 경우는 tls_proxy가 http요청 처리를 위한 80번과 https를 위한 443번 port를 listening하고 있다.

$ docker-compose ps
    Name                  Command               State     Ports                     
------------------------------------------------------------------
tls_apiui      ./webservices/start.sh           Exit 0                                                                                                                                                     
tls_core       ./start.sh                       Up                                                                                                                                                         
tls_mongo      docker-entrypoint.sh --tls ...   Up       127.0.0.1:27017->27017/tcp                                                                                                                        
tls_openvino   /bin/bash                        Exit 0                                                                                                                                                     
tls_proxy      /docker-entrypoint.sh /bin ...   Up       0.0.0.0:443->443/tcp,:::443->443/tcp, 80/tcp                                                                                                      
tls_rabbitmq   docker-entrypoint.sh /init ...   Up       0.0.0.0:15672->15672/tcp,:::15672->15672/tcp, 15691/tcp, 15692/tcp, 0.0.0.0:1883->1883/tcp,:::1883->1883/tcp, 25672/tcp, 4369/tcp, 5671/tcp, 5672/tcp, 0.0.0.0:8883->8883/tcp,:::8883->8883/tcp
tls_redis      docker-entrypoint.sh sh st ...   Up       127.0.0.1:6379->6379/tcp

Docker의 log 보기: docker logs tls_proxy

tls_proxy가 요청을 처리하지 못한 이유는 docker logs <서버 이름>으로 확인할 수 있다.

$ docker logs tls_proxy
2023/05/31 01:31:06 [error] 12#12: *5 connect() failed (113: Host is unreachable) while connecting to upstream, client: 172.31.0.1, server: , request: "GET / HTTP/1.1", upstream: "http://172.31.0.8:3000/", host: "localhost"
...

사설 네트워크인 172.31.0.x로 docker service들을 구성했는데, 그중 인터페이스를 담당하는 172.31.0.1 docker가 172.31.0.8에 요청을 연결해 주어야 하는데 이 부분에서 오류가 나고 있는 모양이다.

Docker network 확인: docker network ls

현재 docker용으로 구성되어 있는 network을 확인해 보면 다음과 같다.

$ docker network ls
NETWORK ID     NAME                DRIVER    SCOPE
d15b28e7a1d5   bridge              bridge    local
acc99b5947d1   cvat_cvat           bridge    local
4485972f8274   host                host      local
6f21c18ba66f   none                null      local
20d132ccfc2a   tls_default         bridge    local

각 네트워크의 구성을 검사(inspect)해 볼 수 있는데 docker network inspect cvat_cvat명령어로 해당 네트워크를 검사해 보면 다음과 같이 위에서 오류가 발생했던 ip인 172.21.0.8번(tls_openvino)이 등록되지 않은 것을 볼 수 있다.

$ docker network inspect cvat_cvat|grep "Name\|IPv4Address"
        "Name": "cvat_cvat",
                "Name": "cvat_redis",
                "IPv4Address": "172.31.0.4/16",
                "Name": "cvat_opa",
                "IPv4Address": "172.31.0.3/16",
                "Name": "cvat_db",
                "IPv4Address": "172.31.0.5/16",
                "Name": "traefik",
                "IPv4Address": "172.31.0.2/16",
                "Name": "cvat",
                "IPv4Address": "172.31.0.6/16",
                "Name": "cvat_ui",
                "IPv4Address": "172.31.0.7/16",
                "Name": "tls_proxy",
                "IPv4Address": "172.31.0.9/16",

Docker의 log 보기: docker logs tls_openvino

문제가 되고 있는 docker container를 특정했으니 docker logs tls_openvino명령어로 다시 한번 해당 container의 로그를 확인해 본다.

error: A hook (`userconfig`) failed to load!
error: Failed to lift app: Error: Attempted to `require('/home/tls/webservices/apiserver/config/env/production.js')`, but an error occurred:
--
Error: ENOENT: no such file or directory, open '../../thirdparty/security/ca/ca_certificate.pem'
    at Object.openSync (fs.js:498:3)
    at Object.readFileSync (fs.js:394:35)
    at Object.<anonymous> (/home/tls/webservices/apiserver/config/env/production.js:70:15)

../../third party/security/ca/ca_certificate.pem파일을 읽지 못해서 오류가 발생했다고 나오는데, 실제로 container에서는 해당 파일의 위치를 상대 경로로 접근하지 않는다. Call stack에 나와 있는 production.js line 70근처의 내용을 보면 먼저 절대경로인 /run/secrets/*에 접근을 시도할 때 예외가 발생해서 상대 경로로 접근을 시도하고(개발용 코드로 추정) 이 마저도 실패한 것이 로그에 나온 것이다.

apiserver/config/env/production.js

try {
  tls_ca = fs.readFileSync("/run/secrets/ca_tls")
  tls_server_cert = fs.readFileSync("/run/secrets/tlsserver_cert")
  tls_server_key = fs.readFileSync("/run/secrets/tlsserver_key")
} catch (err) {
  tls_ca = fs.readFileSync("../../thirdparty/security/ca/ca_certificate.pem");
  tls_server_cert = fs.readFileSync("../../thirdparty/security/TLS_server_cert.crt");
  tls_server_key = fs.readFileSync("../../thirdparty/security/TLS_server_key.pem");
}

정리하자면, 문제는 server secret을 생성하는 과정에서 발생한 것이 docker image상의 key file path read에서 오류를 발생시키고 tls_openvino container가 제대로 뜨지 못한 문제이다.

해결(?)

문제의 원인은 생각보다 싱거웠는데, docker-compose 명령어를 실행시키는 native용 script에서 다음 줄을 연결하기 위한 \를 빼먹는 사소한(!) Bash 문법 오류가 발생 했었고, 이 오류가 무시된 채로 docker-compose build 명령어가 계속 수행되어 service가 up되는 상황까지 된 것이었다.

-       && pip install -U pip wheel setuptools
+       && pip install -U pip wheel setuptools \
        && pip install \

위의 수정과 함께 오류가 생기면 build를 멈추도록 set -e를 bash script에 추가 하는 것으로 이 문제는 일단락 되었다.

Linux에서 메모리 포인터의 유효성 검증

Windows에서와 달리 Linux환경에서는 딱히 포인터의 유효성을 검증할 수 있는 system call이 없다. 이 포스팅은 Linux환경에서 이와 유사한 기능을 구현하기 위해 “정보의 바다”에서 찾은 내용들을 정리해 둔 것이다.

1. _etext를 이용하는 방법

첫번째 방법은 Define and use a pointer validation function 이라는 위키문서에서 가져온 것인데 컴파일러가 생성하는 text 영역의 시작점을 이용해서 포인터 값이 이를 침범 하는지 여부를 검사하고 유효성을 판단한다. 하지만 위 링크의 커멘트에도 나와 있듯이 시스템에 따라 동작하지 않을 수도 있으므로 reliable 한 구현이라 볼 수는 없다.

bool isValidPointer_ET(void *ptr) {
    extern const char _etext;
    return (ptr != nullptr) && ((const char*)ptr > &_etext);
}

2. msync()를 이용하는 방법

Checking whether a pointer is valid in Linux라는 블로그 포스트에 소개된 방법으로 매핑된 메모리 공간을 동기화 할 때 쓰는 msync() 시스템 콜을 호출 하면서 유효하지 않은 page 시작 주소를 넘겨 주면 0이 아닌 음수 값을 반환하는 것을 이용하는 방법이다. (이 경우 errno에 ENOMEM이 설정된다)

#include <sys/mman.h>
#include <unistd.h>

bool isValidPointer_MS(void *ptr) {
    const size_t pageSize = sysconf(_SC_PAGESIZE);
    void *basePtr = (void *)((((size_t)ptr) / pageSize) * pageSize);
    return msync(basePtr, pageSize, MS_SYNC) == 0;
}

3. mincore()를 이용하는 방법

Stackoverflow에 올려진 Testing pointers for validity (C/C++)라는 질문에 대한 답변에 있는 아이디어 중 하나인데 메모리 페이지의 swap 상태를 확인해서 반환해 주는 mincore()을 이용하는 방법이다. 해당 답변에는 다른 아이디어 들도 있으니 필요에 따라 참고.

#include <sys/mman.h>
#include <unistd.h>

bool isValidPointer_MC(void *ptr) {
    unsigned char vec = 0;
    const size_t pageSize = sysconf(_SC_PAGESIZE);
    void *basePtr = (void *)((((size_t)ptr) / pageSize) * pageSize);
    int ret = mincore(basePtr, pageSize, &vec);
    return (ret == 0 && ((vec & 0x1) == 0x1));
}

시험 결과와 결론

위의 함수들에 대해 Linux환경에서 전역 변수 포인터, 지역 변수 포인터, 널 포인터, 널 포인터는 아니지만 명백하게 무효한 포인터(0x04 같은), 동적 할당된 공간에 대한 유효성 여부는 잘 동작한다.

하지만, 이미 해제된 포인터나 할당되지 않은 heap 공간 내의 임의 주소에 대해서는 제대로 유효성 여부를 판단하지 못하고, 주소 범위가 유효 하다면 포인터 역시 유효 하다고 판단하는 오류가 세가지 구현 모두에 있다.

    // 해제된 heap공간에 대한 유효성 여부 확인. 모두 실패함.
    unsigned int* dynamicVar = new unsigned int[100];
    delete[] dynamicVar;
    EXPECT_FALSE(isValidPointer_ET(dynamicVar));
    EXPECT_FALSE(isValidPointer_MS(dynamicVar));
    EXPECT_FALSE(isValidPointer_MC(dynamicVar));


    // 할당 되지 않은 Heap공간 내의 임의 포인터에 대한 유효성 확인. 모두 실패함.
    unsigned int* dynamicUnallocVar = dynamicVar + 100;
    EXPECT_FALSE(isValidPointer_ET(dynamicUnallocVar));
    EXPECT_FALSE(isValidPointer_MS(dynamicUnallocVar));
    EXPECT_FALSE(isValidPointer_MC(dynamicUnallocVar)); 

즉, 위의 구현 들은 주어진 포인터가 유효한 메모리 공간내에 속하는지는 확인할 수 있어도, 동적 할당 영역의 메모리 포인터가 실제 read/write 가능한 상태인지 여부는 정확히 반환 할 수 없다.

시험에 사용한 code는 여기에 붙여 둔다.

GDB에서 인스턴스의 실제 타입 표시하기

Base class로 부터 상속 받은 Derived class의 인스턴스를 Base class 포인터에 넣으면 GDB에서 타입이 제대로 표시되지 않는다. 이 때는 ‘set print object on’을 설정해서 GDB의 ptype <var> 명령 결과에 해당 인스턴스의 실제 타입이 함께 표시 되도록 할 수 있다.

예를 들어 다음과 같은 코드가 있다고 할 때

#include <iostream>
using namespace std;

class Base {
public:
  virtual void identify() {
      cout << "Base class" << endl;
  }
};

class Derived: public Base {
public:
  void identify() override {
      cout << "Derived class" << endl;
  }
};


int main(void) {
  // Derived class instance 이지만
  // GDB에서 Base type으로 표시된다.
  Base *c = new Derived();
  c->identify();

  delete c;
}

main() 함수 내의 “Base *c”를 GDB에서 확인해 보면 실제 인스턴스의 타입과 관련 없이 Base class로 표시된다.

(gdb) ptype c
type = class Base {
  public:
    virtual void identify(void);
} *

실제 인스턴스가 무엇인지 알고 싶다면, set print ojbect on을 설정해서 ptype 결과에 실제 인스턴스가 함께 표시되도록 할 수 있다. ( /* real type = … */)

(gdb) set print object on
(gdb) ptype c
type = /* real type = Derived * */
class Base {
  public:
    virtual void identify(void);
} *

이 설정을 다른 유용한 것들과 함께 ~/.gdbinit에 넣어두면 gdb가 실행될 때 자동으로 설정된다.

$ cat ~/.gdbinit
set print object on
set print pretty on
set print static-members on
set print vtbl on

Gitlab test-runner의 FATAL: the “HOME” is not set 문제

GitLab Runner 문서를 보고 열심히 따라하고 설정까지 마쳤는데 러너가 동작하지 않고 commit을 하면 CI가 한 참 동안을 pending 상태에 있다가 실패했다는 에러 메일 보내 버리는 증상이 있다. Systemctl로 서비스 상태를 보니 제대로 올려지지 않고 ‘inactive’이거나 ‘activating’에 계속 머물러 있다. 하지만 만약 background 서비스로 동작시키지 않고 다음과 같이 run command로 foreground에서 돌리면 잘 동작한다. 환경 설정에는 ‘거의’ 문제가 없다는 뜻 이겠지..

$ sudo /usr/local/bin/gitlab-runner run

Journalctl로 서비스 로그를 보니 FATAL에러가 하나가 잡힌다.

$ journalctl -u gitlab-runner.service

...
gitlab-runner[4622]: FATAL: the "HOME" is not set                      
systemd[1]: gitlab-runner.service: Main process exited, code=exited, status=1/FAILURE
systemd[1]: gitlab-runner.service: Failed with result 'exit-code'.

오호라. HOME 환경 변수를 찾으려 했는데 없어서 죽은 거고만! 해결책은 간단히 HOME 환경 변수를 선언해 주면 된다. 서비스 설정파일 (/etc/systemd/system/gitlab-runner.service)를 열고 [Service] 항목에 환경 변수를 추가해 준다. 만약 proxy환경이고 환경변수로 설정하고 있다면 서비스에서는 환경변수를 접근할 수 없으므로 여기에 함께 선언해 준다.

[Unit]
Description=GitLab Runner
After=syslog.target network.target
ConditionFileIsExecutable=/usr/local/bin/gitlab-runner
 
[Service]
Environment=HOME=/home/gitlab-runner # 이 부분을 추가
# 해당하는 경우 proxy 설정
#Environment=no_proxy=노프록시 설정
#Environment=NO_PROXY=노프록시 설정
#Environment=http_proxy=HTTP프록시 주소
#Environment=HTTP_PROXY=HTTP프록시 주소
#Environment=https_proxy=HTTPS프록시 주소
#Environment=HTTPS_PROXY=HTTPS프록시 주소
StartLimitInterval=5
StartLimitBurst=10
ExecStart=/usr/local/bin/gitlab-runner "run" "--working-directory" "/home/gitlab-runner" "--config" "/etc/gitlab-runner/config.toml" "--service" "gitlab-runner" \
  "--syslog" "--user" "gitlab-runner"

Restart=always
RestartSec=120

[Install]
WantedBy=multi-user.target

이제 서비스를 다시 로드하고 Gitlab runner를 재 시작한다.

$ sudo systemctl daemon-reload
$ sudo /usr/local/bin/gitlab-runner restart

Systemctl에서 상태를 확인한다. 물론 CI test도 함께.

$ sudo systemctl status gitlab-runner.service 
● gitlab-runner.service - GitLab Runner
   Loaded: loaded (/etc/systemd/system/gitlab-runner.service; enabled; vendor preset: disabled)
   Active: active (running) since xxx; 1min 55s ago
 Main PID: 12954 (gitlab-runner)
    Tasks: 9 (limit: 4915)
   Memory: 7.9M
   CGroup: /system.slice/gitlab-runner.service
           └─12954 /usr/local/bin/gitlab-runner run --working-directory /home/gitlab-runner --config /etc/gitlab-runner/config.toml --service gitlab-runner