Post

Django에서 os.system 사용하기

이전에 Cockpit이라는 웹 GUI 서버 관리 콘솔을 봤던 적이 있다.
최신 운영체제에서는 활성화를 시키는 것으로 사용할 수 있었는데, 타지에서 내 서버에 접근할 때 굳이 SSH 클라이언트를 통하지 않아도 웹으로 서버 관리를 하는 것이 마음에 들어서 자주 사용했다.

개요

Django로 개발을 하다 보니 변동 사항을 확인하려면 웹 애플리케이션을 다시 실행해야 하는 불편함이 있어 관리자 페이지에 웹 애플리케이션을 다시 시작하는 버튼을 만들었다.

첫 시도 - 의식의 흐름

파이썬에서는 os.system으로 운영체제에 명령어를 전달할 수 있으므로 요런식으로 간단하게 해결하고자 했다.

1
2
3
import os

os.system('sudo systemctl restart {webapp}')

문제

작성하고 보니 sudo 권한은 어디서 들고 와야 할지 난감해졌는데, www-data의 Shell을 살리는 방법은 버리고 아예 권한도 들고 있고 django도 실행하고 있는 계정으로 전부 옮겼다.

그럼에도 sudo에는 패스워드가 필요했고, 텍스트로 패스워드를 저장하는 것에 부담을 느껴서 그냥 패스워드 없이 sudo를 실행할 권한을 줬다.(!!)

1
echo "{username}	ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers

두 번째 시도와 실패

다시 시도해봤는데, 이상한 문제가 생겼다. sudo 명령어를 찾을 수 없다고 한다.

1
-bash: sudo: command not found

아니 잘 쓰는 sudo를 왜 못쓰니..?

bash에 대해서…

아후의 처절한 삽질은 뒤로 하고 결론부터 말하자면, bash는 단지 os에게 입력을 하고 출력을 보여주는 도구에 불과하다는 것이다.

예를 들어 우리가

1
sudo nginx -t

이렇게 작성하면 bash는 /usr/bin/sudo와 /usr/sbin/nginx -t 를 각각 실행하는 것이다.
Linux 환경에서 저 경로들이 $PATH에 들어있기 때문에 곧바로 실행이 가능한 것이다.

경로는 아래와 같은 명령어로 확인할 수 있다.

1
2
3
4
5
$ type sudo
sudo is hashed (/usr/bin/sudo)

$ type apt
apt is /usr/bin/apt

따라서 $PATH에 경로를 추가하던지, 경로를 모두 입력해줘야 해당 명령어를 사용할 수 있다.

1
/usr/bin/sudo /usr/bin/systemctl restart {webapp}

이렇게 사용해야 하는 것이다.

삽질 과정

평소에 숨쉬듯 쓰던 명령어들이 입력이 안되니 정말 머리가 아팠다.

ls도 안되고 이것저것 해봤는데 되는 게 없어서 아예 bash가 실행이 정상적으로 안된건가 싶었다.

ls 정도는 기본 명령어라고 생각했다.

subprosscess로 넘어가서 excutable=”/bin/bash”도 줘보고 해도 안됐다.

어디에서 실행은 되는지 보자 하고 pwd를 쳐봤고, 여기서 실마리가 잡혔다.
파이썬 가상환경 경로가 잡힌 것이다.

명령어의 성공이 일단 있었으니 쉘은 확실히 켜진 것을 확인했고, 그러면 되는 명령어는 도대체 무엇이 기준인가 싶어서 몇 가지 더 입력해보다가 help를 입력하고 해결됐다.

$ help
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
These shell commands are defined internally.  Type `help' to see this list.
Type `help name' to find out more about the function `name'.
Use `info bash' to find out more about the shell in general.
Use `man -k' or `info' to find out more about commands not in this list.

A star (*) next to a name means that the command is disabled.

 job_spec [&]                            history [-c] [-d offset] [n] or hist>
 (( expression ))                        if COMMANDS; then COMMANDS; [ elif C>
 . filename [arguments]                  jobs [-lnprs] [jobspec ...] or jobs >
 :                                       kill [-s sigspec | -n signum | -sigs>
 [ arg... ]                              let arg [arg ...]
 [[ expression ]]                        local [option] name[=value] ...
 alias [-p] [name[=value] ... ]          logout [n]
 bg [job_spec ...]                       mapfile [-d delim] [-n count] [-O or>
 bind [-lpsvPSVX] [-m keymap] [-f file>  popd [-n] [+N | -N]
 break [n]                               printf [-v var] format [arguments]
 builtin [shell-builtin [arg ...]]       pushd [-n] [+N | -N | dir]
 caller [expr]                           pwd [-LP]
 case WORD in [PATTERN [| PATTERN]...)>  read [-ers] [-a array] [-d delim] [->
 cd [-L|[-P [-e]] [-@]] [dir]            readarray [-d delim] [-n count] [-O >
 command [-pVv] command [arg ...]        readonly [-aAf] [name[=value] ...] o>
 compgen [-abcdefgjksuv] [-o option] [>  return [n]
 complete [-abcdefgjksuv] [-pr] [-DEI]>  select NAME [in WORDS ... ;] do COMM>
 compopt [-o|+o option] [-DEI] [name .>  set [-abefhkmnptuvxBCHP] [-o option->
 continue [n]                            shift [n]
 coproc [NAME] command [redirections]    shopt [-pqsu] [-o] [optname ...]
 declare [-aAfFgiIlnrtux] [-p] [name[=>  source filename [arguments]
 dirs [-clpv] [+N] [-N]                  suspend [-f]
 disown [-h] [-ar] [jobspec ... | pid >  test [expr]
 echo [-neE] [arg ...]                   time [-p] pipeline
 enable [-a] [-dnps] [-f filename] [na>  times
 eval [arg ...]                          trap [-lp] [[arg] signal_spec ...]
 exec [-cl] [-a name] [command [argume>  true
 exit [n]                                type [-afptP] name [name ...]
 export [-fn] [name[=value] ...] or ex>  typeset [-aAfFgiIlnrtux] [-p] name[=>
 false                                   ulimit [-SHabcdefiklmnpqrstuvxPT] [l>
 fc [-e ename] [-lnr] [first] [last] o>  umask [-p] [-S] [mode]
 fg [job_spec]                           unalias [-a] name [name ...]
 for NAME [in WORDS ... ] ; do COMMAND>  unset [-f] [-v] [-n] [name ...]
 for (( exp1; exp2; exp3 )); do COMMAN>  until COMMANDS; do COMMANDS; done
 function name { COMMANDS ; } or name >  variables - Names and meanings of so>
 getopts optstring name [arg ...]        wait [-fn] [-p var] [id ...]
 hash [-lr] [-p pathname] [-dt] [name >  while COMMANDS; do COMMANDS; done
 help [-dms] [pattern ...]               { COMMANDS ; }

여기에 포함된 명령어들만이 bash에서 기본적으로 내릴 수 있는 명령어인 것이다.

그래서 Linux로 돌아와서 type으로 경로를 찾았고, 그대로 입력해서 잘 동작하는 것을 확인했다.

참고로 type은 bash 자체 내장 명령어로, 경로가 따로 없다.
자체 내장 명령어의 위치를 찾으면 아래와 같이 나온다.

1
2
3
4
5
$ type type
type is a shell builtin

$ whereis type
type:

결론

결론적으로 아래와 같이 작성하여 웹 애플리케이션 재시작 버튼을 만들었다.

bash 스크립트 파일

1
2
#!/bin/bash
/usr/bin/sudo /usr/bin/systemctl restart {webapp}

view.py

1
2
3
4
5
6
...
import subprecess
...
    if request.POST.get('restart') == 'True':
        subprocess.run('{bash 스크립트}', shell=True, executable="/bin/bash")
...

index.html

1
2
3
4
5
6
7
  {% if user.username == {username} %}
  <form class="p-2" action="{% url '{app}:index' %}" method="post">
    {% csrf_token %}
    <input type="hidden" name="restart" value="True">
    <button class="btn btn-outline-danger" type="submit">RESTART WEB</button>
  </form>
  {% endif %}

해결 과제

  1. 웹 서비스를 실행하는 계정에게 줘버린 NOPASSWD 권한 해결
  2. restart 값이 True로 넘어오는 것과 관리자 로그인 여부를 AND 조건으로 확인

참고

GNU - Bash Reference Manual

This post is licensed under CC BY 4.0 by the author.