Pacemakerによる冗長構成を試す
PacemakerはLinuxで使われる冗長化ソフトウェアです。複数ノードでACT/SBY構成などによる冗長を確保したい場合に使用されます。 今回は仮想環境上に3ノードのクラスターを構成し、Apaceh Webサーバを実行します。
VMを下記の通り作成します。
■cluster1.example.net
IP:192.168.11.121/24
■cluster2.example.net
IP:192.168.11.122/24
■cluster3.example.net
IP:192.168.11.123/24
Pacemakerの構築はこのサイトを参考にさせていただきました。今回は複数ノードあるため、構成管理ツールであるansibleで環境を構築します。 作業用の環境にansibleをインストールし、下記のplaybookをcluster_setup.ymlという名前で作成します。
- hosts: cluster_nodes become: yes vars: - CLUSTER_PASS: 'cluster_passw0rd' tasks: - name: Update packages latest yum: name: '*' state: latest - name: Install pacemaker packages yum: name: pacemaker,pcs,fence-agents,httpd state: installed - name: Start services systemd: name: "{{ item }}" enabled: yes state: started with_items: - pcsd - name: Enable Corosync and Pacemaker service systemd: name: "{{ item }}" enabled: yes with_items: - corosync - pacemaker - name: Change hacluster password user: name: hacluster password: "{{ CLUSTER_PASS | password_hash('sha512') }}" - name: Allow Pacemaker Connection firewalld: service: "{{ item }}" permanent: yes immediate: yes state: enabled with_items: - high-availability - http - name: Check hosts file shell: "grep 192.168.11.12 /etc/hosts | wc -l" register: test_hosts - name: Add cluster hosts to /etc/hosts lineinfile: path: "/etc/hosts" state: present line: | 192.168.11.121 cluster1.example.net 192.168.11.122 cluster2.example.net 192.168.11.123 cluster3.example.net when: test_hosts.stdout == "0" - name: Configure HTTPD status page copy: dest: "/etc/httpd/conf.d/status.conf" content: | ExtendedStatus On <Location /server-status> SetHandler server-status Require local </Location> - name: Make sure Apache server disable by default systemd: name: httpd enabled: no state: stopped - name: Add Test content shell: "echo \"running on `hostname`\" > /var/www/html/test.txt"
inventory.txtを作成し、作業対象のサーバを記述します。
[cluster_nodes] 192.168.11.121 192.168.11.122 192.168.11.123
playbookを実行します。
[jinglan@ansible-server ~]# ansible-playbook -i inventory.txt cluster_setup.yml --ask-become-pass -vv
しばらくすると環境構築が終わります。結果にfailedが表示されていないことを確かめてください。
次からの作業はクラスタのうち一つにログインして作業します。3つのノードを登録しクラスターとして動作させます。
[root@cluster1 ~]# pcs cluster auth cluster1.example.net cluster2.example.net cluster3.example.net -u hacluster Password: (playbookで定義したパスワードを入力) cluster2.example.net: Authorized cluster1.example.net: Authorized cluster3.example.net: Authorized [root@cluster1 ~]# pcs cluster setup --name httpcluster cluster1.example.net cluster2.example.net cluster3.example.net Destroying cluster on nodes: cluster1.example.net, cluster2.example.net, cluster3.example.net... cluster3.example.net: Stopping Cluster (pacemaker)... cluster1.example.net: Stopping Cluster (pacemaker)... cluster2.example.net: Stopping Cluster (pacemaker)... cluster1.example.net: Successfully destroyed cluster cluster2.example.net: Successfully destroyed cluster cluster3.example.net: Successfully destroyed cluster Sending 'pacemaker_remote authkey' to 'cluster1.example.net', 'cluster2.example.net', 'cluster3.example.net' cluster1.example.net: successful distribution of the file 'pacemaker_remote authkey' cluster2.example.net: successful distribution of the file 'pacemaker_remote authkey' cluster3.example.net: successful distribution of the file 'pacemaker_remote authkey' Sending cluster config files to the nodes... cluster1.example.net: Succeeded cluster2.example.net: Succeeded cluster3.example.net: Succeeded Synchronizing pcsd certificates on nodes cluster1.example.net, cluster2.example.net, cluster3.example.net... cluster2.example.net: Success cluster1.example.net: Success cluster3.example.net: Success Restarting pcsd on the nodes in order to reload the certificates... cluster2.example.net: Success cluster1.example.net: Success cluster3.example.net: Success [root@cluster1 ~]# pcs cluster start --all cluster1.example.net: Starting Cluster (corosync)... cluster2.example.net: Starting Cluster (corosync)... cluster3.example.net: Starting Cluster (corosync)... cluster1.example.net: Starting Cluster (pacemaker)... cluster3.example.net: Starting Cluster (pacemaker)... cluster2.example.net: Starting Cluster (pacemaker)... [root@cluster1 ~]# pcs property set stonith-enabled=false [root@cluster1 ~]#
クラスターを構成したので現在の状態を確認します。
[root@cluster1 ~]# pcs status Cluster name: httpcluster Stack: unknown Current DC: NONE Last updated: Sat Jun 27 15:55:09 2020 Last change: Sat Jun 27 15:55:04 2020 by root via cibadmin on cluster1.example.net 3 nodes configured 0 resources configured OFFLINE: [ cluster1.example.net cluster2.example.net cluster3.example.net ] No resources Daemon Status: corosync: active/disabled pacemaker: active/disabled pcsd: active/enabled
3つのノードが認識されていますが、リソースが設定されていないためOFFLINE状態になっています。 そのため、クラスターで冗長化するリソースについて設定します。今回は冗長化されたIPアドレス(192.168.11.120)とHTTPサーバを定義します。
[root@cluster1 ~]# pcs resource create VIP ocf:heartbeat:IPaddr2 ip=192.168.11.120 cidr_netmask=24 op monitor interval=10s on-fail="standby" --group httpgroup [root@cluster1 ~]# pcs resource create HTTPD ocf:heartbeat:apache configfile="/etc/httpd/conf/httpd.conf" statusurl="http://127.0.0.1/server-status" --group httpgroup
HTTPDとVIPは同じリソースグループに属しているので一緒に切り替わります。 再度、pacemakerの状態を表示します。
[root@cluster1 ~]# pcs status Cluster name: httpcluster Stack: corosync Current DC: cluster1.example.net (version 1.1.21-4.el7-f14e36fd43) - partition with quorum Last updated: Sat Jun 27 10:25:59 2020 Last change: Fri Jun 26 22:00:25 2020 by root via cibadmin on cluster1.example.net 3 nodes configured 2 resources configured Online: [ cluster1.example.net cluster2.example.net cluster3.example.net ] Full list of resources: Resource Group: httpgroup VIP (ocf::heartbeat:IPaddr2): Started cluster1.example.net HTTPD (ocf::heartbeat:apache): Started cluster1.example.net Daemon Status: corosync: active/enabled pacemaker: active/enabled pcsd: active/enabled
Resource Groupの表示に「Started cluster1.example.net」と記載があり、cluster1で実行されていることがわかります。実際にVIPアドレスへHTTPリクエストを飛ばして確認してみます。
$ curl http://192.168.11.120/test.txt running on cluster1.example.net
応答からcluster1でHTTP処理が行われたことが分かります。 続いてcluster1を疑似的に停止します。その前に、別ターミナルから連続でHTTPリクエストを飛ばし、切り替え状態をモニターします。
$ while : ; do curl http://192.168.11.120/test.txt ; sleep 1; done; running on cluster1.example.net
VMを実行しているホストにて下記コマンドでVMを停止します。
# virsh suspend cluster1
モニターしているターミナルでは一時疎通不可になったのち、切り替わることが確認できます。
running on cluster1.example.net running on cluster1.example.net curl: (7) Failed to connect to 192.168.11.120 port 80: Connection refused curl: (7) Failed to connect to 192.168.11.120 port 80: Connection refused running on cluster2.example.net running on cluster2.example.net running on cluster2.example.net
次に、cluster3を停止します。
# virsh suspend cluster3
モニター側ではWebサイトからの応答がなくなります。 cluster2側で状態を確認します。
[jinglan@cluster2 ~]$ sudo pcs status Cluster name: httpcluster Stack: corosync Current DC: cluster2.example.net (version 1.1.21-4.el7-f14e36fd43) - partition WITHOUT quorum Last updated: Sat Jun 27 13:54:09 2020 Last change: Sat Jun 27 13:52:53 2020 by root via cibadmin on cluster1.example.net 3 nodes configured 2 resources configured Online: [ cluster2.example.net ] OFFLINE: [ cluster1.example.net cluster3.example.net ] Full list of resources: Resource Group: httpgroup VIP (ocf::heartbeat:IPaddr2): Stopped HTTPD (ocf::heartbeat:apache): Stopped Daemon Status: corosync: active/enabled pacemaker: active/enabled pcsd: active/enabled
Resource Groupが停止していることが確認できます。3ノード構成ではquorumによりアクティブになるノードが決定されます。これにより、2ノードから疎通ができなくなったノードは冗長構成から切り離されたと判断し、リソースの競合を防ぐため停止します。
停止させたcluster1とcluster3を再開します。
# virsh resume cluster1 # virsh resume cluster3
cluster1とcluster3が復帰したことでリソースはcluster1に戻ります。
[root@cluster1 ~]# pcs status Resource Group: httpgroup VIP (ocf::heartbeat:IPaddr2): Started cluster1.example.net HTTPD (ocf::heartbeat:apache): Started cluster1.example.net
特定のホストでリソースを実行したい場合はstandbyを使います。cluster1とcluster2をstandby状態にして、cluster3で実行させるようにします。
[root@cluster1 ~]# pcs cluster standby cluster1.example.net [root@cluster1 ~]# pcs cluster standby cluster2.example.net [root@cluster1 ~]# pcs status Cluster name: httpcluster Stack: corosync Current DC: cluster3.example.net (version 1.1.21-4.el7-f14e36fd43) - partition with quorum Last updated: Sat Jun 27 15:59:00 2020 Last change: Sat Jun 27 15:58:54 2020 by root via cibadmin on cluster1.example.net 3 nodes configured 2 resources configured Node cluster1.example.net: standby Node cluster2.example.net: standby Online: [ cluster3.example.net ] Full list of resources: Resource Group: httpgroup VIP (ocf::heartbeat:IPaddr2): Started cluster3.example.net HTTPD (ocf::heartbeat:apache): Started cluster3.example.net
リソースはcluster3で実行を続けます。なお、この時にcluster3が停止状態になるとリソースの移動先がないために切り替わりが起きません。 standby状態はunstandbyコマンドにより解除することができます。
[root@cluster1 ~]# pcs cluster unstandby --all
standbyによるACT/SBY構成の切り替えはシングル障害点になってしまいます。特定のノードにリソースを移したい場合はresource moveにより切り替えます。
[root@cluster1 ~]# pcs resource move httpgroup cluster1.example.net [root@cluster1 ~]# pcs status (略) Resource Group: httpgroup VIP (ocf::heartbeat:IPaddr2): Started cluster1.example.net HTTPD (ocf::heartbeat:apache): Started cluster1.example.net
次に、リソース起動に失敗した場合の動作について確認してみます。Apacheの設定ファイルを取り除きます。
[root@cluster1 ~]# cd /etc/httpd/conf/ [root@cluster1 conf]# mv httpd.conf httpd.conf.bak
現在動いているプロセスをkillします。
[root@cluster1 ~]# ps aux | grep httpd root 24998 0.0 0.3 224080 3468 ? Ss Jun27 0:18 /sbin/httpd -DSTATUS -f /etc/httpd/conf/httpd.conf -c PidFile /var/run/httpd.pid [root@cluster1 ~]# kill 24998
ログを確認すると起動に失敗したため系切り替えが発生したことが分かります。
[root@cluster1 conf]# tail /var/log/messages Jun 29 22:24:19 cluster1 lrmd[22953]: notice: HTTPD_start_0:17745:stderr [ ocf-exit-reason:Configuration file /etc/httpd/conf/httpd.conf not found! ] Jun 29 22:24:19 cluster1 lrmd[22953]: notice: HTTPD_start_0:17745:stderr [ ocf-exit-reason:environment is invalid, resource considered stopped ] Jun 29 22:24:19 cluster1 crmd[22956]: notice: Result of start operation for HTTPD on cluster1.example.net: 5 (not installed) Jun 29 22:24:19 cluster1 crmd[22956]: notice: cluster1.example.net-HTTPD_start_0:24 [ ocf-exit-reason:Configuration file /etc/httpd/conf/httpd.conf not found!\nocf-exit-reason:environment is invalid, resource considered stopped\n ] Jun 29 22:24:19 cluster1 apache(HTTPD)[17773]: ERROR: Configuration file /etc/httpd/conf/httpd.conf not found! Jun 29 22:24:19 cluster1 apache(HTTPD)[17773]: INFO: environment is invalid, resource considered stopped Jun 29 22:24:19 cluster1 lrmd[22953]: notice: HTTPD_stop_0:17773:stderr [ ocf-exit-reason:Configuration file /etc/httpd/conf/httpd.conf not found! ] Jun 29 22:24:19 cluster1 crmd[22956]: notice: Result of stop operation for HTTPD on cluster1.example.net: 0 (ok) Jun 29 22:24:19 cluster1 IPaddr2(VIP)[17800]: INFO: IP status = ok, IP_CIP= Jun 29 22:24:19 cluster1 crmd[22956]: notice: Result of stop operation for VIP on cluster1.example.net: 0 (ok)
現在の状態を確認します。
[root@cluster1 conf]# pcs status Cluster name: httpcluster Stack: corosync Current DC: cluster3.example.net (version 1.1.21-4.el7-f14e36fd43) - partition with quorum Last updated: Mon Jun 29 22:24:27 2020 Last change: Sat Jun 27 16:07:21 2020 by root via crm_resource on cluster1.example.net 3 nodes configured 2 resources configured Online: [ cluster1.example.net cluster2.example.net cluster3.example.net ] Full list of resources: Resource Group: httpgroup VIP (ocf::heartbeat:IPaddr2): Started cluster2.example.net HTTPD (ocf::heartbeat:apache): Started cluster2.example.net Failed Resource Actions: * HTTPD_start_0 on cluster1.example.net 'not installed' (5): call=24, status=complete, exitreason='environment is invalid, resource considered stopped', last-rc-change='Mon Jun 29 22:24:19 2020', queued=0ms, exec=36ms
起動に失敗したことがFailed Resource Actionsに表示されています。取り除いたApacheの設定ファイルを元に戻します。
mv httpd.conf.bak httpd.conf
これでcluster1のApacheは正常に起動できるはずです。cluster2とcluster3をstanbyにしてみます。
[root@cluster1 conf]# pcs cluster standby cluster2.example.net [root@cluster1 conf]# pcs cluster standby cluster3.example.net [root@cluster1 conf]# pcs status Cluster name: httpcluster Stack: corosync Current DC: cluster3.example.net (version 1.1.21-4.el7-f14e36fd43) - partition with quorum Last updated: Mon Jun 29 22:34:30 2020 Last change: Mon Jun 29 22:34:13 2020 by root via cibadmin on cluster1.example.net 3 nodes configured 2 resources configured Node cluster2.example.net: standby Node cluster3.example.net: standby Online: [ cluster1.example.net ] Full list of resources: Resource Group: httpgroup VIP (ocf::heartbeat:IPaddr2): Started cluster1.example.net HTTPD (ocf::heartbeat:apache): Stopped Failed Resource Actions: * HTTPD_start_0 on cluster1.example.net 'not installed' (5): call=24, status=complete, exitreason='environment is invalid, resource considered stopped', last-rc-change='Mon Jun 29 22:24:19 2020', queued=0ms, exec=36ms
HTTPDについてはリソースは停止したままです。たとえノードがOnline状態であっても、Failed Resource Actionsとして傷害の記録が残っているものは切り替わりません。そのため、下記コマンドで障害の状態をリセットする必要があります。
[root@cluster1 conf]# pcs resource cleanup Cleaned up VIP on cluster3.example.net Cleaned up VIP on cluster2.example.net Cleaned up VIP on cluster1.example.net Cleaned up HTTPD on cluster3.example.net Cleaned up HTTPD on cluster2.example.net Cleaned up HTTPD on cluster1.example.net Waiting for 1 reply from the CRMd. OK [root@cluster1 conf]# pcs status Cluster name: httpcluster Stack: corosync Current DC: cluster3.example.net (version 1.1.21-4.el7-f14e36fd43) - partition with quorum Last updated: Mon Jun 29 22:41:35 2020 Last change: Mon Jun 29 22:41:30 2020 by hacluster via crmd on cluster1.example.net 3 nodes configured 2 resources configured Node cluster2.example.net: standby Node cluster3.example.net: standby Online: [ cluster1.example.net ] Full list of resources: Resource Group: httpgroup VIP (ocf::heartbeat:IPaddr2): Started cluster1.example.net HTTPD (ocf::heartbeat:apache): Started cluster1.example.net
Failed Resource Actionsについては見落としやすいので注意したほうがよいです。
Open vSwitchでパケットロスのトラブルシューティング
Open vSwitch(OVS)を使った仮想マシン上で特定のサイトでWebページの読み込みができない事象が発生しました。 仮想環境の構成は以下の通りです。
VM側では下記のように外部セグメント(br-outside)と内部セグメント(br-inside)に接続されています。
jinglan@vm-linux:~$ ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 52:54:00:bf:71:af brd ff:ff:ff:ff:ff:ff inet 172.16.0.8/16 brd 172.31.255.255 scope global enp1s0 valid_lft forever preferred_lft forever inet6 fe80::5054:ff:febf:71af/64 scope link valid_lft forever preferred_lft forever 3: enp7s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 52:54:00:ae:d2:98 brd ff:ff:ff:ff:ff:ff inet6 2400::feae/64 scope global dynamic mngtmpaddr valid_lft 2591933sec preferred_lft 604733sec inet6 fe80::d298/64 scope link valid_lft forever preferred_lft forever続きを読む
Zabbixでログ監視環境を構築する
少し前に話題になった入門監視を読みました。
- 作者:Mike Julian
- 発売日: 2019/01/17
- メディア: 単行本(ソフトカバー)
どういった監視項目があるのか、実用的な監視環境や方式についてノウハウを学べる良い書籍だと思います。 この本の中でsyslogについて触れられていました。
昔は、シンプルなsyslog受信サーバである中央ログサーバにすべてのログを送り、Unix系OSで標準的なツール(例えばgrepなど)でログを検索していました。これはログを検索しにくいという点で次善の策でした。この方法でログを保存すると、たいていの場合は誰もログを見ず、活用することもありません。(p. 127)
書かれている通りで、ログを収集してもいちいちログを確認したりはしないものです。とはいえ、再起動でログがリセットされるCisco系装置やサーバでログが失われるようなトラブルが発生したときにはログ収集サーバは有益です。
ここで重要なのは、2度と見られることはないので、ログをsyslogサーバに送ってはいけないということです。その代わり、ログを活用して価値を得られるしっかりしたログ管理システムに送りましょう。(p. 128)
ログ管理システムはクラウドやオンプレなど多岐に渡りますが、今回はZabbixで簡易的な管理・監視環境を構築してみます。
続きを読むPython + Djangoで動画共有サイトを作る Part4
前回の続きです。視聴ページとデプロイをします。
視聴ページ作成
動画を再生する視聴ページを作成します。HTML5ではvideoタグを使うことでブラウザがUIを提供してくれます。
- video/templates/video/watch.html
{% extends "video/base.html" %} {% block title %}{{ content.title }} - Video{% endblock %} {% block header %} {% load static %} <script src="{% static 'video/jquery-3.4.1.min.js' %}"></script> <script src="{% static 'video/watch.js' %}"></script> {% endblock %} {% block main %} <br> <br> <h2>{{ content.title }}</h2><br> {{ content.description }} <hr> tags: {% for tag in tags %} <a href="../tag/{{ tag.tag.name }}">{{ tag.tag.name }}</a> {% endfor %} <br> <hr> <video id="video_content" src="/media/video/{{ content.id }}/{{ content.filename}}" poster="/media/video/{{ content.id }}/thumb.jpg" controls> 動画の再生にはHTML5が必要です。 </video> <hr> <br> {% endblock %}
再生画面で音量を変更した場合、その値を記憶するようにします。
- video/static/video/watch.js
$(function() { function getCookie() { var cookie = []; if (document.cookie != '') { cookie_list = document.cookie.split(';'); for (var item of cookie_list) { [key, value] = item.split('='); cookie[key.replace(' ', '')] = decodeURIComponent(value); } } return cookie; } function saveCookie(_key, _value) { document.cookie = _key + '=' + encodeURIComponent(_value) + '; max-age=31536000; path=/'; } $(document).ready(function(){ cookie = getCookie(); if (cookie['volume']) { $('#video_content').get(0).volume = cookie['volume']; } }); $('#video_content').on('volumechange', function() { saveCookie('volume', $('#video_content').get(0).volume); }); });
音量の値の記憶にはcookieを用います。
- video/views.py
def watch(request, content_id): content = get_object_or_404(VideoContent, pk=content_id) tags = VideoTagList.objects.filter(content_id=content_id).select_related('content') return render(request, 'video/watch.html', {'content':content, 'tags':tags})
コメントアウトを外した後にrunserverを実行してWebサイトにアクセスします。動画のリンクをクリックすると再生画面に飛びます。
デプロイ
いままで開発環境だったためrunserverを実行して動かしました。今後はHTTPサーバから呼び出す形で実行します。HTTPサーバとしてはapacheを使用します。
#yum install httpd python36u-mod_wsgi policycoreutils-python
インストールが完了したらデーモンとして起動します。
# sudo systemctl status httpd ● httpd.service - The Apache HTTP Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; vendor preset: disabled) Active: inactive (dead) Docs: man:httpd(8) man:apachectl(8) # # sudo systemctl start httpd # sudo systemctl status httpd ● httpd.service - The Apache HTTP Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; vendor preset: disabled) Active: active (running) since Tue 2019-08-27 18:36:40 JST; 2s ago Docs: man:httpd(8) man:apachectl(8) Main PID: 26104 (httpd) Status: "Processing requests..." CGroup: /system.slice/httpd.service tq26104 /usr/sbin/httpd -DFOREGROUND tq26105 /usr/sbin/httpd -DFOREGROUND tq26106 /usr/sbin/httpd -DFOREGROUND tq26107 /usr/sbin/httpd -DFOREGROUND tq26108 /usr/sbin/httpd -DFOREGROUND mq26109 /usr/sbin/httpd -DFOREGROUND Aug 27 18:36:40 localhost.localdomain systemd[1]: Starting The Apache HTTP Server... Aug 27 18:36:40 localhost.localdomain httpd[26104]: AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using l... message Aug 27 18:36:40 localhost.localdomain systemd[1]: Started The Apache HTTP Server. Hint: Some lines were ellipsized, use -l to show in full. # # sudo systemctl enable httpd Created symlink from /etc/systemd/system/multi-user.target.wants/httpd.service to /usr/lib/systemd/system/httpd.service. #
続いて、HTTPサーバでWSGIが動作するように設定を変更します。まずはロケールの設定を変更します。
- /etc/sysconfig/httpd
LANG=ja_JP.utf8
今回は日本語を使用するためロケールの文字コードをUTF8に変更します。この設定を入れないと文字をASCIIとして扱うためUnicodeEncodeError: 'ascii' codec can't encode characters in position 40-43: ordinal not in range(128)というエラーが発生します。
続いてHTTPサーバの設定をします。今回は/var/www/django/以下にファイルを設置します。
WSGIScriptAlias / /var/www/django/mysite/mysite/wsgi.py WSGIPythonPath /var/www/django/mysite/ <Directory "/var/www/django/mysite/video"> <Files wsgi.py> Require all granted </Files> </Directory> Alias /static/ /var/www/django/mysite/video/static/ <Directory "/var/www/django/mysite/video/static"> Require all granted </Directory> Alias /media/ /storage/ <Directory "/storage/"> Require all granted </Directory>
設定が完了したらHTTPサーバを再起動します。
# systemctl restart httpd # sudo systemctl status httpd ● httpd.service - The Apache HTTP Server Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled) Active: active (running) since Tue 2019-08-27 18:53:18 JST; 4s ago Docs: man:httpd(8) man:apachectl(8) Process: 26163 ExecStop=/bin/kill -WINCH ${MAINPID} (code=exited, status=0/SUCCESS) Main PID: 26167 (httpd) Status: "Processing requests..." CGroup: /system.slice/httpd.service tq26167 /usr/sbin/httpd -DFOREGROUND tq26168 /usr/sbin/httpd -DFOREGROUND tq26169 /usr/sbin/httpd -DFOREGROUND tq26170 /usr/sbin/httpd -DFOREGROUND tq26171 /usr/sbin/httpd -DFOREGROUND mq26172 /usr/sbin/httpd -DFOREGROUND Aug 27 18:53:17 localhost.localdomain systemd[1]: Stopped The Apache HTTP Server. Aug 27 18:53:17 localhost.localdomain systemd[1]: Starting The Apache HTTP Server... Aug 27 18:53:17 localhost.localdomain httpd[26167]: AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using l... message Aug 27 18:53:18 localhost.localdomain systemd[1]: Started The Apache HTTP Server. Hint: Some lines were ellipsized, use -l to show in full.
HTTPサーバの設定ファイルで指定したフォルダに作成したdjangoプロジェクトのファイルを設置します。
# mkdir /var/www/django # mkdir /var/www/django/mysite # cp * /var/www/django/mysite/ -R # chown apache:apache /var/www/django/mysite -R # ls -l /var/www/django/mysite /var/www/django/mysite: total 4 -rwxr-xr-x. 1 apache apache 626 Aug 27 19:01 manage.py drwxr-xr-x. 3 apache apache 93 Aug 27 19:01 mysite drwxr-xr-x. 6 apache apache 188 Aug 27 19:01 video #
また、ストレージについてもHTTPサーバからアクセスできるようにします。
# chown apache:apache -R /storage/video/ # semanage fcontext -a -t httpd_sys_rw_content_t '/storage/video(/.*)?' # restorecon -v -R /storage/video/
djangoの設定も公開用に書き換えます。
- mysite/settings.py
# SECURITY WARNING: don't run with debug turned on in production! DEBUG = False ALLOWED_HOSTS = ['192.168.12.9']
djangoではデプロイ時に設定をチェックする機能があります。
$ python3 manage.py check --deploy
必要に応じて警告の内容を修正します。修正後は設定内容を反映するためHTTPサーバを再起動する必要があります。
# systemctl restart httpd
サーバの設定が完了したのでファイアウォールの設定を変更します。
$ sudo firewall-cmd --info-zone=public public (active) target: default icmp-block-inversion: no interfaces: enp0s3 sources: services: ssh dhcpv6-client ports: 8000/tcp protocols: masquerade: no forward-ports: source-ports: icmp-blocks: rich rules: $ sudo firewall-cmd --add-service http --zone=public --permanent success $ sudo firewall-cmd --reload success $ sudo firewall-cmd --info-zone=public public (active) target: default icmp-block-inversion: no interfaces: enp0s3 sources: services: ssh dhcpv6-client http ports: 8000/tcp protocols: masquerade: no forward-ports: source-ports: icmp-blocks: rich rules:
今までのrunserverではhttp://192.168.12.9:8000/でアクセスしていましたが、HTTPサーバに配置したのでhttp://192.168.12.9/にアクセスします。 正常に表示・動作できていればOKです。
トラブルシューティング
ファイルが存在するのに500 Internal Errorになる
サーバのエラーに関する情報はerror_logに記録されています。
# tailf /var/log/httpd/error_log [Tue Aug 27 19:13:30.382093 2019] [core:error] [pid 26296] (13)Permission denied: [client 192.168.12.4:57900] AH00035: access to /media/video/3/thumb.jpg denied (filesystem path '/storage/video/3/thumb.jpg') because search permissions are missing on a component of the path, referer: http://192.168.12.9/video/ [Tue Aug 27 19:13:31.346448 2019] [wsgi:error] [pid 26296] [client 192.168.12.4:57900] Not Found: /favicon.ico
Permission deniedの場合は権限に由来するものです。拒否されたファイルを調査します。
# ls -lZ storage/video/3/ -rw-------. root root unconfined_u:object_r:user_tmp_t:s0 video.mp4 -rw-------. root root unconfined_u:object_r:default_t:s0 thumb.jpg
apache(HTTPサーバ)が所有者になっていない場合はchmodコマンドで所有者を変更します。
# chmod apache:apache /storage/video/3/video.mp4 # chmod apache:apache /storage/video/3/thumb.jpg # ls -lZ /storage/video/3/ -rw-------. apache apache unconfined_u:object_r:user_tmp_t:s0 video.mp4 -rw-------. apache apache unconfined_u:object_r:default_t:s0 thumb.jpg
また、SELinuxの権限が異なる場合もアクセスが拒否されます。SELinuxが動作しているかは次のコマンドで確認できます。
# getenforce Enforcing
EnforcingであればSELinuxが動作しています。SELinuxは一時的に停止することができます。
# setenforce 0 # getenforce Permissive
PermissiveであればSELinuxによるアクセス拒否は発生しません。アクセスに関するログはaudit.logに記録されています。
# tailf /var/log/httpd/error_log type=AVC msg=audit(1566901506.108:729): avc: denied { getattr } for pid=26343 comm="httpd" path="/storage/video/3/thumb.jpg" dev="dm-0" ino=17209734 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=1
HTTPサーバのコンテキストsystem_u:system_r:httpd_tではファイルのコンテキストunconfined_u:object_r:default_t:s0を読みことができないため拒否(denied)されています。一時的にファイルのコンテキストを変更してみます。
# chcon -t httpd_sys_content_t /storage/video/3/thumb.jpg # ls -lZ /storage/video/3/ -rw-------. apache apache unconfined_u:object_r:user_tmp_t:s0 video.mp4 -rw-------. apache apache unconfined_u:object_r:httpd_sys_content_t thumb.jpg
これでコンテキストが変更されたので再度ブラウザからアクセスし、audit.logに拒否の記録が残るかどうか確かめます。問題なく表示された場合はSELinuxの設定を戻します。
# setenforce 1 # getenforce Enforcing
chconは一時的なものなので新しくSELinuxのルールを作成します。
# semanage fcontext -a -t httpd_sys_rw_content_t '/storage/video(/.*)?' # semanage fcontext -l | grep /storage/ /storage/video(/.*)? all files system_u:object_r:httpd_sys_rw_content_t:s0
続いて、設定したルールを適用します。
# restorecon -v -R /storage/video/ restorecon reset /storage/video/3/video.mp4 context unconfined_u:object_r:user_tmp_t:s0->unconfined_u:object_r:httpd_sys_rw_content_t:s0
他のファイルのコンテキストもルールに従って再設定されました。
Python + Djangoで動画共有サイトを作る Part3
前回の続きです。前回はトップページを作成しましたので、残りを作っていきます。
タグ検索
タグ検索はURLが/tag/(検索文字列)の場合に検索文字列に合致する動画を返します。 動画の表示ページは前回construct_page関数として作ったので、表示する動画一覧を作って渡します。
- video/views.py
def tag(request, tag_name, page=0): # tag_nameからIDを探し、見つかったIDを基にタグが付いた動画をフィルタする tag_id = VideoTagName.objects.filter(name=tag_name).get().id filtered_list = VideoTagList.objects.select_related('content').filter(tag=tag_id).order_by('-content__upload_date') max_page = filtered_list.count() // 10 content_list = filtered_list[page*10:(page+1)*10] contents = [{'id':item.content.id, 'title':item.content.title} for item in content_list] return construct_page(request, filtered_list.values('content_id'), contents, page, max_page, 'video:tag', tag_name)
urls.pyのコメントアウトを外した後にrunserverを実行してWebサイトにアクセスします。タグのリンクをクリックすると/tag/TAG1のURLに飛び、検索タグに一致する動画が出てきます。
キーワード検索
キーワード検索では検索された文字列を基にデータベースを部分一致検索で探します。
def search(request, search_word, page=0): filtered_list = VideoContent.objects.filter(title__contains=search_word).order_by('-upload_date') max_page = filtered_list.count() // 10 content_list = filtered_list[page*10:(page+1)*10] contents = [{'id':item.id, 'title':item.title} for item in content_list] return construct_page(request, filtered_list.values('id'), contents, page, max_page, 'video:search', search_word)
基本的な処理はタグ検索と同様です。データベースからの検索結果をconstruct_page関数に渡しページを作成します。
また、検索窓からのPOSTを受け持つ処理を実装します。
def search_post(request): if hasattr(request, 'POST') and 'search_text' in request.POST.keys(): if request.POST['search_text'] != "": return HttpResponseRedirect(reverse('video:search', args=(request.POST['search_text'],))) return HttpResponseRedirect(reverse('video:index'))
検索ワードから検索ページに飛ばすURLへリダイレクトします。
アップロードページ作成
アップロードされたデータはストレージと呼ばれる保存領域に記録します。今回は/storage/video/というフォルダを作成し保存します。実際はデータストレージはWebサーバと別の筐体という構成にすることで可用性を向上させます。
# mkdir /storage/ # mkdir /storage/video # ls /storage/ -l total 0 drwxr-xr-x. 2 root root 6 Aug 4 17:32 video # chown jinglan:jinglan /storage/video/ $ sudo ls /storage/ -l total 0 drwxr-xr-x. 2 jinglan jinglan 6 Aug 4 17:32 video
フォルダを作成し権限を書き換えました。次に、設定ファイルにこのフォルダをストレージ領域として使うことを記述します。
- mysite/settings.py
# Media directory MEDIA_ROOT = '/storage/' MEDIA_URL = '/media/'
また、URLからアクセスできるようサイトのurls.pyの最後にに追加します。
- mysite/urls.py
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
保存場所の準備ができたのでアップロード画面を作成します。
- video/templates/video/upload.html
{% extends "video/base.html" %} {% block main %} <br> <br> <form action="" method="post" enctype="multipart/form-data"> {% csrf_token %} {{ form.as_p }} <br> <input type="submit" value="アップロード"> </form> <br> {% endblock %}
アップロードページのフォームはform.as_pでviews.pyの定義から自動的に作成されます。views.pyでは以下のように汎用ビューを用いたクラスを追加します。
- video/views.py
from django.urls import reverse, reverse_lazy from django.views import generic from django import forms from django.core.files.storage import default_storage, FileSystemStorage from django.utils import timezone from django.conf import settings import ffmpeg DATA_DIR = settings.MEDIA_ROOT + 'video/' class VideoUploadForm(forms.Form): file = forms.FileField() class UploadView(generic.FormView): form_class = VideoUploadForm template_name = 'video/upload.html' def form_valid(self, form): upload_filename = form.cleaned_data["file"].name content = VideoContent(title=upload_filename, description="", upload_date=timezone.now(), original_name=upload_filename, filename="") content.save() try: storage = FileSystemStorage() storage.location = DATA_DIR + str(content.id) filename = storage.save(upload_filename, form.cleaned_data["file"]) make_video_thumb(DATA_DIR + str(content.id) + "/" + filename, content.thumb_frame, DATA_DIR + str(content.id) + "/thumb.jpg") except: delete_video(content.id, filename) content.delete() raise else: content.filename = filename content.save() return HttpResponseRedirect(reverse('video:edit', args=(content.id,)))
VideoUploadFormクラスはアップロードページのフォームを規定します。今回はファイルのみです。 正常にユーザからファイルがアップロードされた後にform_validが呼ばれます。 content.save()によりデータベースに新しくレコードを追加され、ユニークなIDが割り当てられます。このIDを基に動画を保存するフォルダを作成し保存します。 正常に保存されたらmake_video_thumb関数によりサムネイルを作成します。
- video/views.py
def make_video_thumb(src_filename, capture_frame, dst_filename=None): probe = ffmpeg.probe(src_filename) video_info = next(x for x in probe['streams'] if x['codec_type'] == 'video') nframes = video_info['nb_frames'] avg_frame_rate = (lambda x: int(x[0])/int(x[1])) (video_info['avg_frame_rate'].split('/')) start_position = int(capture_frame)/avg_frame_rate if dst_filename == None: out_target = 'pipe:' else: out_target = dst_filename im = ( ffmpeg.input(src_filename, ss=start_position) .filter('scale', 200, -1) .output(out_target, vframes=1, format='image2', vcodec='mjpeg', loglevel='warning') .overwrite_output() .run(capture_stdout=True) ) return im
ffmpegを使用し指定されたフレームの画像を生成しています。
もし、アップロード処理中にエラーが発生した場合はexceptが呼ばれ、delete_video関数により動画は削除されます。
- video/views.py
def delete_video(content_id, video_filename): print('remove files at ' + str(content_id) + '/') storage = FileSystemStorage() storage.location = DATA_DIR storage.delete(str(content_id) + '/' + video_filename) storage.delete(str(content_id) + '/' + 'thumb.jpg') storage.delete(str(content_id) + '/')
正常に動画をアップロードできた場合はHttpResponseRedirectにより編集ページにリダイレクトされます。今回はまだ作成していないのでダミーページを返すようにします。
- video/views.py
def edit(request, content_id): return HttpResponse("dummy")
それでは、urls.pyを編集してからアップロードしてみます。アップロード後はダミーページに飛ばされますがトップページに戻ると動画リストが増えています。
編集ページ作成
アップロード後に動画の情報を入力する編集ページを作成します。このページでは、動画情報の編集のほかに、サムネイルの変更、タグの追加削除、動画削除機能を提供します。
- video/templates/video/edit.html
{% extends "video/base.html" %} {% block header %} {% load static %} <script src="{% static 'video/jquery-3.4.1.min.js' %}"></script> <script src="{% static 'video/edit.js' %}"></script> {% endblock %} {% block main %} <h2>編集中: {{ content.id }} - {{ content.title }}</h2> <h3>VIDEO</h3> <form action="{% url 'video:update' content.id %}" method="post"> {% csrf_token %} <table border="0"> <tr><td>thumb:</td> <td> <img id="thumb" src="{% url 'video:thumb' content.id content.thumb_frame %}"> <input name="frame" id="thumb_frame" type="range" style="width:500px" min="0" max="{{ video_info.max_frame }}" step="1" value="{{ content.thumb_frame }}"> <span id="current_thumb_frame">{{ content.thumb_frame }} </span><br> </td> </tr><tr><td>title:</td> <td> <input name="title" type="text" size="100" value="{{ content.title }}"><br> </td> </tr><tr><td>description:</td> <td> <textarea name="desc" rows="5" cols="100">{{ content.description }}</textarea> </td> </tr> </table> <br> <input type="submit" value="決定"> </form> <br> <hr> <h3>TAG</h3> <form action="{% url 'video:update_add_tag' content.id %}" method="post"> <table border="0"> {% for tag in tags %} <tr><td>{{ tag.tag.name }}</td><td><input type="submit" value="削除" formaction="{% url 'video:update_remove_tag' content.id tag.tag.name %}"></td></tr> {% endfor %} </table> <input type="text" name="tag" value=""> <input type="submit" value="追加"> {% csrf_token %} </form> <br> <hr> <a href="{% url 'video:delete' content.id %}">削除する</a> <br> {% endblock %}
サムネイルの取得にはⒿjQueryを使い非同期的に行います。jquery-3.4.1.min.jsを公式サイトからダウンロードしstaitcに配置します。 また、サムネイル取得を実行するedit.jsも作成します。
- video/static/video/edit.js
$(function() { $('#thumb_frame').on('input', function() { $('#current_thumb_frame').text($('#thumb_frame').val()); }); $('#thumb_frame').change(function() { $('#current_thumb_frame').text($('#thumb_frame').val()); var image_dir = $('#thumb').attr('src').split('/'); image_dir.pop(); $('#thumb').attr('src', image_dir.join('/') + '/' + $('#thumb_frame').val()); }); });
range入力が変更されるたびにサーバにframe情報を送り、サムネイル画像を取得します。
それではviews.pyに編集ページを実装します。
- video/views.py
def edit(request, content_id): content = get_object_or_404(VideoContent, pk=content_id) probe = ffmpeg.probe(DATA_DIR + str(content.id) + "/" + content.filename) video_info = next(x for x in probe['streams'] if x['codec_type'] == 'video') info = {'max_frame': video_info['nb_frames']} tags = VideoTagList.objects.filter(content_id=content_id).select_related('content') return render(request, 'video/edit.html', {'content':content, 'video_info':info, 'tags':tags})
urls.pyを編集し、編集ページにアクセスします。ページは表示されますがサムネイルやボタンは動作を実装していないため動きません。
では、まず最初にサムネイルから実装します。サムネイルは/video/edit/(動画ID)/thumb/(フレーム)というURLで読み込まれます。
- video/views.py
def thumb(request, content_id, frame): content = get_object_or_404(VideoContent, pk=content_id) im = make_video_thumb(DATA_DIR + str(content.id) + "/" + content.filename, frame) return HttpResponse(im, content_type="image/jpeg")
URLを基にサムネイルを作成し画像であるimage/jpegとして返します。
これでスライダーを変更したときに動的にサムネイル画像をプレビューできるようになりました。 続いて、変更した値を保存する処理を実装します。
- video/views.py
def update(request, content_id): content = get_object_or_404(VideoContent, pk=content_id) content.title = request.POST['title'] content.thumb_frame = request.POST['frame'] content.description = request.POST['desc'] content.save() make_video_thumb(DATA_DIR + str(content.id) + "/" + content.filename, content.thumb_frame, DATA_DIR + str(content.id) + "/thumb.jpg") return HttpResponseRedirect(reverse('video:index'))
これで編集ページで書き換えた値やサムネイルが一覧に反映されるようになりました。
今度はタグの編集機能を実装します。
- video/views.py
def update_add_tag(request, content_id): if request.POST["tag"] != "": tag = VideoTagName.objects.filter(name=request.POST["tag"]) if len(tag) == 0: tag = VideoTagName(name=request.POST["tag"]) tag.save() else: tag = tag[0] tag_list = VideoTagList.objects.filter(tag_id=tag.id, content_id=content_id) if len(tag_list) == 0: tag_list = VideoTagList(tag_id=tag.id, content_id=content_id) tag_list.save() return HttpResponseRedirect(reverse('video:edit', kwargs={'content_id': content_id}))
タグの追加では、まずタグが存在するか確認し存在しない場合はVideoTagNameに追加します。その後、VideoTagListに動画IDとタグIDを紐づけて登録します。
- video/views.py
def update_remove_tag(request, content_id, tag_name): tag = VideoTagName.objects.filter(name=tag_name) if len(tag) != 0: tag_list = VideoTagList.objects.filter(tag_id=tag[0].id, content_id=content_id) tag_list.delete() return HttpResponseRedirect(reverse('video:edit', kwargs={'content_id': content_id}))
タグの削除では、動画IDとタグIDが一致するレコードをVideoTagListから削除します。
それでは、実際に動かしてみます。
編集画面からTAG1と新しいTAG3を付けることができました。
最後に、動画の削除機能を実装します。
- video/templates/video/delete.html
{% extends "video/base.html" %} {% block main %} <br> <br> <form method="post"> {% csrf_token %} <img src="/media/video/{{ object.id }}/thumb.jpg"><br> {{ object.title }}を削除します。<br><br> <input type="submit" value="削除"> </form> {% endblock %}
削除処理自体は以前に作成したdelete_video関数を利用します。
- video/views.py
class DeleteView(generic.DeleteView): model = VideoContent template_name = 'video/delete.html' success_url = reverse_lazy('video:index') def delete(self, request, *args, **kwargs): content_id = self.kwargs['pk'] filename = VideoContent.objects.filter(id=content_id)[0].filename delete_video(content_id, filename) return super().delete(request, *args, **kwargs)
削除するボタンを押すとストレージから動画自体が削除され、データベースから登録が消去されます。
Djangoで動画共有サイトを作る Part4へ続きます。