夜明け前の最も暗いとき

技術的なやったことをメモするブログ

Python + Djangoで動画共有サイトを作る Part2

前回の続きです。引き続きPythonDjangoで作っていきます。

ページの表示

まず、Django内にアプリケーションとしてプロジェクトを作成します。

$ python3 manage.py startapp video
$ ls
manage.py  mysite  video

これで新しくvideoフォルダができました。これからは主にvideoフォルダにあるファイルを編集していきます。フォルダの中は下記のようになっています。

$ ls video/
admin.py  apps.py  __init__.py  migrations  models.py  tests.py  views.py

新しくアプリケーションのurls.pyを作成します。urls.pyはブラウザからリクエストされたURLをどのように処理するのかを記述します。

  • video/urls.py
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from . import views

app_name = 'video'
urlpatterns = [
    path('', views.index, name='index'),
]

まずはインデックス("/")のみのURLを取り扱い、views.pyに記載されたindex関数を呼び出して処理します。name='index'は後ほど'index'という文字からURLを生成できるようするため設定します。 また、video/urls.pyをルートのURL設定に反映する必要があります。

  • mysite/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('video/', include('video.urls')),
    path('admin/', admin.site.urls),
]

次に、urls.pyから呼び出されるindex関数をviews.pyに定義します。

  • video/views.py
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, HttpResponseRedirect

def index(request):
    return render(request, 'video/index.html')

render関数はHTMLのひな形を読み込んで内容を動的に生成し表示します。ひな形はアプリケーションのtemplateフォルダに設置します。

  • video/templates/video/index.html
{% extends "video/base.html" %}
{% block main %}
Hello, World
{% endblock %}

テンプレートを利用する利点の一つはは共通部をまとめることが可能な点です。extendsはvideo/base.htmlを参照しblockを必要に応じて置き換えます。

  • video/templates/video/base.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="robots" content="noindex,nofollow">
  <meta http-equiv="Pragma" Content="no-cache">
  <meta http-equiv="Cache-Control" Content="no-cache">
  <title>{% block title %}Video Manager{% endblock %}</title>
{% load static %}
  <link rel="stylesheet" type="text/css" href="{% static 'video/main.css' %}">
{% block header %}{% endblock %}
</head>
<body>

<h1><a href="{% url 'video:index' %}">Video Service</a></h1>
{% block main %}

{% endblock %}
</body>
</html>

urlテンプレートタグは指定した文字列を基にurls.pyを探索しURLを返します。これによって、URLの変更をurls.pyですれば各々のテンプレートを変更せずとも全体に反映されるようになります。 また、cssファイルのような動的に生成されないものについてはアプリケーションのstaticフォルダに設置します。

  • video/static/video/main.css
body {
        background-color:#FFFFFF;
}

最後に、テンプレートを有効にするためsettings.pyのINSTALLED_APPSにvideo.apps.VideoConfigを追加します。

  • mysite/settings.py
# Application definition

INSTALLED_APPS = [
    'video.apps.VideoConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

それでは、runserverを実行してhttp://192.168.12.9:8000/video/にブラウザから接続します。 問題がなければHello, Worldが表示されます。

サイトの設計

動画サイトを作成するにあたってどのような要素が必要になるのか検討します。 動画情報として以下の要素が挙げられます。

  • 管理ID
  • タイトル
  • 説明文
  • タグ情報
  • アップロード日
  • ファイル名

データベースにはこれらの情報が保存される必要があります。 タグ情報は複数つけることができます。また、複数の動画に同じタグが付けられます。 これは動画の管理IDとタグの管理IDを紐づけることで実現します。

以上の検討したデータベースをmodel.pyにクラスとして記述します。

  • video/model.py
from django.db import models

class VideoContent(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField()
    upload_date = models.DateTimeField()
    original_name = models.CharField(max_length=200)
    filename = models.CharField(max_length=200, default="")
    thumb_frame = models.IntegerField(default=0)

class VideoTagName(models.Model):
    name = models.CharField(max_length=200, default="")

class VideoTagList(models.Model):
    content = models.ForeignKey(VideoContent, on_delete=models.CASCADE)
    tag = models.ForeignKey(VideoTagName, on_delete=models.CASCADE)

変更をデータベースに反映します。

$ python3 manage.py makemigrations video
Migrations for 'video':
  video/migrations/0001_initial.py
    - Create model VideoContent
    - Create model VideoTagName
    - Create model VideoTagList
$ python3 manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, video
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying sessions.0001_initial... OK
  Applying video.0001_initial... OK

これでデータベースに作成したクラスからテーブルが作成されました。

次に、どのようなページが必要か検討します。おおよそ以下のページがあると良さそうです。

  • トップページ
  • 検索結果
  • アップロードページ
  • 動画情報編集ページ
  • 動画削除ページ
  • 視聴ページ

各ページではURLに動画IDやタグ名を必要とします。これらを考慮してurls.pyを書き換えます。

  • video/urls.py
urlpatterns = [
    path('', views.index, name='index'),
#    path('<int:page>', views.index, name='index'),
#    path('tag/<str:tag_name>', views.tag, name='tag'),
#    path('tag/<str:tag_name>/<int:page>', views.tag, name='tag'),
#    path('search/', views.search_post, name='search'),
#    path('search/<str:search_word>', views.search, name='search'),
#    path('search/<str:search_word>/<int:page>', views.search, name='search'),
#    path('watch/<int:content_id>/', views.watch, name='watch'),
#    path('upload/', views.UploadView.as_view(), name='upload'),
#    path('edit/<int:content_id>', views.edit, name='edit'),
#    path('edit/<int:content_id>/thumb/<int:frame>', views.thumb, name='thumb'),
#    path('delete/<int:pk>', views.DeleteView.as_view(), name='delete'),
#    path('update/<int:content_id>', views.update, name='update'),
#    path('update/tag/<int:content_id>', views.update_add_tag, name='update_add_tag'),
#    path('update/tag/<int:content_id>/<str:tag_name>', views.update_remove_tag, name='update_remove_tag'),
]

コメントアウトしてある部分はviews.pyにまだ実装していない箇所であるため、実装したら外します。

トップページの作成

トップページはタグ表示領域と動画リストの2カラムで構成します。関連タグを左側、動画のリストを右側で表示します。表示するページ数は10とし、下部にページナビゲーションを付けます。

  • video/templates/video/index.html
{% extends "video/base.html" %}

{% block main %}

<div class="left">
  <br>
  <a href="./upload/">Upload Video</a>
  <br><br>
  <h4>関連タグ</h4>
  <ul class="taglist">
  {% for item in tags %}
    <li><a href="{% comment %}{% url 'video:tag' item.name %}{% endcomment %}">{{ item.name }}</a> ({{ item.count }})</li>
  {% endfor %}
  </ul>
</div>

<div class="center">
  <form action="{% comment %}{% url 'video:search' %}{% endcomment %}" method="post">
    {% csrf_token %}
    <input name="search_text" type="text" size="50" value="">
    <input type="submit" value="検索">
  </form>
  <br>
  {% for item in contents %}
    <div class="content">
      <a href="{% comment %}{% url 'video:watch' item.id %}{% endcomment %}"><img src="/media/video/{{ item.id }}/thumb.jpg"></a>
      <div class="detail">
        <a href="{% comment %}{% url 'video:watch' item.id %}{% endcomment %}"><h4>{{ item.title }}</h4></a>
        <br>
        {% for tag in item.tags %}
          <a href="{% comment %}{% url 'video:tag' tag.tag.name %}{% endcomment %}">{{ tag.tag.name }}</a>
        {% endfor %}
        <br>
        <br>
        <a href="{% comment %}{% url 'video:edit' item.id %}{% endcomment %}">Edit</a>
      </div>
    </div>
    {% endfor %}

  <br>

  {% if page.word != '' %}
    <a href="{% url page.type page.word 0 %}">最初</a>/
  {% else %}
    <a href="{% url page.type 0 %}">最初</a>/
  {% endif %}

  {% for item in page.list %}
    {% if item.valid %}
      {% if item.num == page.current %}
        <b>{{ page.current }}</b>/
      {% else %}
        {% if page.word != '' %}
          <a href="{% url page.type page.word item.num %}">{{ item.num }}</a>/
        {% else %}
          <a href="{% url page.type item.num %}">{{ item.num }}</a>/
        {% endif %}
      {% endif %}
    {% endif %}
  {% endfor %}

  {% if page.word != '' %}
    <a href="{% url page.type page.word page.max %}">最後</a><br>
  {% else %}
     <a href="{% url page.type page.max %}">最後</a><br>
  {% endif %}
  <br><br>
</div>

{% endblock %}

{% url ~ %}についてはurls.pyで実装していないと逆引きできないため、実装した後に{% comment %}~{% endcomment %}を外して有効にします。 テンプレートではrender関数に渡す辞書変数を使い動的にページを生成します。tags, contents, pageについてはviews.pyで定義します。

2カラムにするためのcssを書いていきます。

  • video/static/video/css.main
h4 {
        margin: 0px;
        padding:0px;
}

 /* 左サイドバー */
.left {
        float: left;
        width: 200px;
        margin: 0px 20px 0px 5px;
}
.taglist{
        margin : 0px;
        padding: 10px 25px;
        border: solid 1px #CCCCCC;
}

/* 真ん中 */
.center {
        min-width: 600px;
        position: absolute;
        left: 200px;
        border: solid 1px white;
        margin: 0px 10px 10px 20px;
        overflow: visible;
}
.footer{
        clear: both;
        overflow :hidden;
}
div .content {
        margin : 2px;
        padding: 3px;
        border: solid 0px #6666FF;
        border-top-width: 1px;
        border-bottom-width: 1px;
        min-height: 150px;
        position: relative;
}
div .content:hover {
        background-color: #DDDDFF;
}
div .detail {
        min-width: 200px;
        position: absolute;
        left: 200px;
        top: 0px;
        margin: 10px 10px 10px 20px;
        overflow: visible;
}

/* Watchページ */
video {
        min-width:320px;
        min-height:240px;
        width: 100%
}

続いてviews.pyを編集してサーバ側の動作を記述します。トップページではタグを多い順で関連タグとして表示し、最近投稿された順で動画を表示します。

index関数ではページの情報や表示する動画のリストを用意します。

  • video/views.py
from .models import VideoContent, VideoTagList, VideoTagName

def index(request, page=0):
    max_page = VideoContent.objects.count() // 10
    return construct_page(request, VideoTagList.objects.values('content_id'), VideoContent.objects.order_by('-upload_date')[page*10:(page+1)*10].values(), page, max_page, 'video:index')

トップページは処理がタグ検索やキーワード検索と重なるところが多いため実質的なページの作成はconstruct_page関数で実施します。 views.pyに以下のconstruct_page関数を追加します。

  • video/views.py
from django.db.models import Count

def construct_page(request, all_content_ids, page_contents, current_page, max_page, url_type, url_word=''):
    # page_contents(動画)に関連するタグを抜き出し、テンプレートで使えるよう整形
    contents = []
    for item in page_contents:
        tmp_dict = item
        tmp_dict.update({'tags': VideoTagList.objects.filter(content_id=item['id']).select_related('tag')})
        contents.append(tmp_dict)

    # all_content_idsからタグを多い順で集計し、整形する
    tag_cnt = VideoTagList.objects.filter(content__in = all_content_ids).values('tag').annotate(tag_count=Count('tag')).order_by('-tag_count')[:10]
    tag_names = [VideoTagName.objects.filter(id = item.get('tag'))[0] for item in tag_cnt]
    tags = [{'name': tag_names[i].name, 'count': tag_cnt[i]["tag_count"]} for i in range(len(tag_names))]

    # ページが有効な範囲をvalidでマークを付ける
    page_list = [{'num':x, 'valid':0 <= x and x <= max_page} for x in range(current_page-5, current_page+4)]

    return render(request, 'video/index.html', {'tags': tags, 'contents': contents, 'page':{'type':url_type, 'word': url_word, 'current': current_page, 'max': max_page, 'list': page_list}})

ORマッパーは初めて使いましたがSQLを知っていると苦戦しますね。

表示処理を書いたので動作させてみます。Djangoでは管理用ページからデータベースを操作することができます。まずは管理者用のアカウントを作成します。

$ python3 manage.py createsuperuser
Username (leave blank to use 'jinglan'): admin
Email address:
Password:
Password (again):
Superuser created successfully.
$

続いて、アプリケーションのモデルを登録します。表示するモデルや項目はadmin.pyに記述します。

  • video/admin.py
from django.contrib import admin
from .models import VideoContent, VideoTagName, VideoTagList


class VideoContentAdmin(admin.ModelAdmin):
    list_display = ('id', 'title', 'upload_date')

class VideoTagListAdmin(admin.ModelAdmin):
    list_display = ('content', 'tag')

class VideoTagNameAdmin(admin.ModelAdmin):
    list_display = ('name',)

admin.site.register(VideoContent, VideoContentAdmin)
admin.site.register(VideoTagList, VideoTagListAdmin)
admin.site.register(VideoTagName, VideoTagNameAdmin)

管理用ページの設定が終わったので表示します。ブラウザからhttp://192.168.12.9:8000/admin/にアクセスします。 認証後、以下のようにモデルが表示されデータベースの操作ができます。

f:id:jianlan:20190804163016j:plain

Video contentにaddボタンからレコードを追加します。

f:id:jianlan:20190804163034j:plain

Video tagにTAG1とTAG2レコードを追加します。

f:id:jianlan:20190804163121j:plain

登録した動画とタグをVideo TagListで紐づけます。

f:id:jianlan:20190804163253j:plain

アプリケーションに戻ってトップページを表示します。

f:id:jianlan:20190813011315j:plain

動画リスト及び関連するタグが表示され、トップページが完成しました。

Djangoで動画共有サイトを作る Part3へ続きます。

 

トラブルシューティング

ORマッパーのクエリを確認する。

どうしてもSQLの動作を確認したい時があります。Djangoではshellからコマンドをインタラクティブに確認できる機能を持っています。

$ python3 manage.py shell
Python 3.6.8 (default, Apr 25 2019, 21:02:35)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-36)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

ORマッパーオブジェクトのqueryを参照することでSQL文を表示できます。

>>> from video.models import VideoContent, VideoTagList, VideoTagName
>>> rec = VideoTagList.objects.filter(content_id=1)
>>> print(rec.query)
SELECT `video_videotaglist`.`id`, `video_videotaglist`.`content_id`, `video_videotaglist`.`tag_id` FROM `video_videotaglist` WHERE `video_videotaglist`.`content_id` = 1

生成されたSQLの動作はコマンドラインから確認することができます。

$ mysql django_db -u django -p
Enter password:

MariaDB [django_db]> SELECT `video_videotaglist`.`id`, `video_videotaglist`.`content_id`, `video_videotaglist`.`tag_id` FROM `video_videotaglist` WHERE `video_videotaglist`.`content_id` = 1;
+----+------------+--------+
| id | content_id | tag_id |
+----+------------+--------+
|  1 |          1 |      1 |
+----+------------+--------+
1 row in set (0.00 sec)

Python + Djangoで動画共有サイトを作る Part1

趣味で作っている動画がいっぱいになってきました。複数PCから視聴やタグ検索できると便利だと思ったのでWebサーバで動く動画共有サイトPythonDjangoを作ります。

目的

Webサイトを製作するにあたって以下の項目を考慮します。

  • サムネイルを設定・表示できる
  • タグで検索できる

また、DjangoをつかってWebフレームワークの使い方を習得します。 環境は以下の通りです。

Djangoに関しては以下の公式ドキュメントが参考になりますので一読を勧めます。

docs.djangoproject.com

開発環境の準備

今回は仮想マシン上にLinuxサーバ(CentOS)を構築し、Webサーバを動かします。そのため、仮想環境としてVirtualBoxをインストールします。また、CentOSのインストールイメージもダウンロードします。 仮想マシンは以下のスペックを割り当てました。

  • CPU:2コア
  • メモリ:2GB
  • HDD:15GB
  • IDEプライマリ:CentOS ISOファイル
  • ネットワーク:ブリッジ

f:id:jianlan:20190719155335j:plain

ネットワークはNATのままではホストから接続することができないため、必ず変更します。設定が完了したら仮想マシンを立ち上げ、CentOSをインストールします。

f:id:jianlan:20190719160210j:plain

HDDは仮想マシン作成時にに割り当てた領域をすべて使用します。

f:id:jianlan:20190719160226j:plain

DHCPの有効なLANではネットワークアドレスが自動的に設定されます。必要に応じて固定IPを設定することも可能です。

f:id:jianlan:20190719160234j:plain

今回はIPアドレスを手動で192.168.12.9に設定しました。(開発PCも192.168.12.0/24のセグメントにあります)

インストール完了後、再起動するとCentOSが起動します。CentOSはデフォルトでSSHサーバが有効なため、インストール中に設定したIPアドレスに対してSSHでログインが可能です。ゲストにログインしたらソフトウェアを最新の状態にします。*1

# yum check-update
# yum update

続いて、必要なソフトウェアをダウンロードします。Python3はデフォルトのリポジトリに存在しないため、IUS Community Projectのリポジトリを追加します。

# yum install https://centos7.iuscommunity.org/ius-release.rpm

Python3をインストールします。

# yum install gcc python36 python36-devel python36-pip

pipを使って今回使用するDjangoをインストールします。

# pip3 install django

下記コマンドで正しくインストールできたか確認します。

$ python3 -m django --version
2.2.3
$

続いて、サムネイル作成に使う動画処理ソフトウェアのffmpegをインストールします。

# rpm --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro
# rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-1.el7.nux.noarch.rpm
# yum install ffmpeg

今回はデータベースとしてmysql (mariadb)を使用します。インストールした後デーモンとして起動します。

# yum install mariadb mariadb-devel mariadb-server
# systemctl start mariadb
# systemctl status mariadb
● mariadb.service - MariaDB database server
   Loaded: loaded (/usr/lib/systemd/system/mariadb.service; disabled; vendor preset: disabled)
   Active: active (running) since Sat 2019-08-03 22:30:49 JST; 13s ago
  Process: 4715 ExecStartPost=/usr/libexec/mariadb-wait-ready $MAINPID (code=exited, status=0/SUCCESS)
  Process: 4636 ExecStartPre=/usr/libexec/mariadb-prepare-db-dir %n (code=exited, status=0/SUCCESS)
 Main PID: 4714 (mysqld_safe)
   CGroup: /system.slice/mariadb.service
           tq4714 /bin/sh /usr/bin/mysqld_safe --basedir=/usr
           mq4876 /usr/libexec/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib64/mysql/plugin --log-error=/var/log/mariadb/mariadb.lo...


# systemctl enable mariadb
Created symlink from /etc/systemd/system/multi-user.target.wants/mariadb.service to /usr/lib/systemd/system/mariadb.service.
#

Pythonで連携するため必要なパッケージをインストールします。

# pip3 install mysqlclient ffmpeg-python

インストールが完了したらDjangoのプロジェクトを作成します。

$ django-admin startproject mysite
$

mysiteフォルダが生成されるので、移動します。

$ cd mysite/
$ ls
manage.py  mysite
$

中にはmanage.pyとmysiteフォルダができています。mysqlを使うのでデータベースの設定をします。

 $ vi mysite/settings.py

デフォルトでは下記のようにsqliteを使う設定になっています。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

mysqlを使うために次のように書き換えます。

DATABASES = {
    'default': {
         'ENGINE': 'django.db.backends.mysql',
         'NAME': 'django_db',
         'USER': 'django',
         'PASSWORD': 'django-password',
         'HOST': 'localhost',
    }
}

また、今回のように開発PCとサーバが別の場合は接続できるようにALLOWED_HOSTSに'*'を加えます。

ALLOWED_HOSTS = ['*']

外部からDjangoへの接続を許可しました。Djangoの時刻はデフォルトでUTCになっているので日本標準時にします。

TIME_ZONE = 'Asia/Tokyo'

次にデータベースを作成します。下記コマンドからデータベースへ接続します。

$ mysql -u root
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 2
Server version: 5.5.60-MariaDB MariaDB Server

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]>

データベースを作成します。作成するときにはUTF-8文字コードとして指定します。

MariaDB [(none)]> CREATE DATABASE django_db CHARACTER SET utf8;

Djangoで使用するためのユーザを作成します。

MariaDB [(none)]> CREATE USER 'django'@'localhost' IDENTIFIED BY 'django-password';
MariaDB [(none)]> GRANT ALL PRIVILEGES ON django_db.* TO 'django'@'localhost';
MariaDB [(none)]> SHOW GRANTS FOR 'django'@'localhost';
+---------------------------------------------------------------------------------------------------------------+
| Grants for django@localhost                                                                                   |
+---------------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'django'@'localhost' IDENTIFIED BY PASSWORD '*E783B4785753FC31264A3BE0AABA7790D0A6080A' |
| GRANT ALL PRIVILEGES ON `django_db`.* TO 'django'@'localhost'                                                 |
+---------------------------------------------------------------------------------------------------------------+

MariaDB [(none)]> 

正常にユーザが作成されていればquitで抜けます。 Djangoで建てるWebサーバに外部からアクセスするにはfirewallに設定を追加する必要があります。

$ sudo firewall-cmd --state
running
$ sudo firewall-cmd --get-active-zones
public
  interfaces: enp0s3
$ sudo firewall-cmd --info-zone=public
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: enp0s3
  sources:
  services: ssh dhcpv6-client
  ports:
  protocols:
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

$ sudo firewall-cmd --add-port 8000/tcp --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
  ports: 8000/tcp
  protocols:
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

firewallの許可ポートに8000/tcpを追加しました。それでは、DjnagoのWebサーバを起動します。

$ python3 manage.py runserver 0:8000
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

August 03, 2019 - 13:54:08
Django version 2.2.3, using settings 'mysite.settings'
Starting development server at http://0:8000/
Quit the server with CONTROL-C.

適切に設定されていればエラー無く実行されます。ブラウザからサーバへアクセスしてロケットのアニメーションが再生していればOKです。

f:id:jianlan:20190803234218j:plain

Djangoで動画共有サイトを作る Part2へ続きます。

 

トラブルシューティング

パッケージのインストールエラー

pipのパッケージインストールで下記エラーが発生した場合は、必要なソフトウェアやライブラリが無い可能性があります。

Installing collected packages: mysqlclient, future, ffmpeg-python
  Running setup.py install for mysqlclient ... error
    ERROR: Command errored out with exit status 1:
     command: /usr/bin/python3.6 -u -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/tmp/pip-install-gb0n8i9b/mysqlclient/setup.py'"'"'; __file__='"'"'/tmp/pip-install-gb0n8i9b/mysqlclient/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record /tmp/pip-record-appckohy/install-record.txt --single-version-externally-managed --compile
         cwd: /tmp/pip-install-gb0n8i9b/mysqlclient/
    Complete output (28 lines):
    running install
    running build
    running build_py
    creating build
    creating build/lib.linux-x86_64-3.6
    creating build/lib.linux-x86_64-3.6/MySQLdb
    copying MySQLdb/__init__.py -> build/lib.linux-x86_64-3.6/MySQLdb
    copying MySQLdb/_exceptions.py -> build/lib.linux-x86_64-3.6/MySQLdb
    copying MySQLdb/compat.py -> build/lib.linux-x86_64-3.6/MySQLdb
    copying MySQLdb/connections.py -> build/lib.linux-x86_64-3.6/MySQLdb
    copying MySQLdb/converters.py -> build/lib.linux-x86_64-3.6/MySQLdb
    copying MySQLdb/cursors.py -> build/lib.linux-x86_64-3.6/MySQLdb
    copying MySQLdb/release.py -> build/lib.linux-x86_64-3.6/MySQLdb
    copying MySQLdb/times.py -> build/lib.linux-x86_64-3.6/MySQLdb
    creating build/lib.linux-x86_64-3.6/MySQLdb/constants
    copying MySQLdb/constants/__init__.py -> build/lib.linux-x86_64-3.6/MySQLdb/constants
    copying MySQLdb/constants/CLIENT.py -> build/lib.linux-x86_64-3.6/MySQLdb/constants
    copying MySQLdb/constants/CR.py -> build/lib.linux-x86_64-3.6/MySQLdb/constants
    copying MySQLdb/constants/ER.py -> build/lib.linux-x86_64-3.6/MySQLdb/constants
    copying MySQLdb/constants/FIELD_TYPE.py -> build/lib.linux-x86_64-3.6/MySQLdb/constants
    copying MySQLdb/constants/FLAG.py -> build/lib.linux-x86_64-3.6/MySQLdb/constants
    running build_ext
    building 'MySQLdb._mysql' extension
    creating build/temp.linux-x86_64-3.6
    creating build/temp.linux-x86_64-3.6/MySQLdb
    gcc -pthread -Wno-unused-result -Wsign-compare -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DNDEBUG -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -D_GNU_SOURCE -fPIC -fwrapv -fPIC -Dversion_info=(1,4,2,'post',1) -D__version__=1.4.2.post1 -I/usr/include/mysql -I/usr/include/python3.6m -c MySQLdb/_mysql.c -o build/temp.linux-x86_64-3.6/MySQLdb/_mysql.o
    unable to execute 'gcc': No such file or directory
    error: command 'gcc' failed with exit status 1
    ----------------------------------------
ERROR: Command errored out with exit status 1: /usr/bin/python3.6 -u -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/tmp/pip-install-gb0n8i9b/mysqlclient/setup.py'"'"'; __file__='"'"'/tmp/pip-install-gb0n8i9b/mysqlclient/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record /tmp/pip-record-appckohy/install-record.txt --single-version-externally-managed --compile Check the logs for full command output.

この場合は'gcc': No such file or directoryとあるので

# yum install gcc

GCCをインストールします。

Djangoの実行に必要なパッケージが不足

データベースが適切に設定されていないと以下のようなエラーになります。

$ python3 manage.py runserver 0:8000
Watching for file changes with StatReloader
Exception in thread django-main-thread:
Traceback (most recent call last):
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/mysql/base.py", line 15, in <module>
    import MySQLdb as Database
ModuleNotFoundError: No module named 'MySQLdb'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/lib64/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/usr/lib64/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib64/python3.6/site-packages/django/utils/autoreload.py", line 54, in wrapper
    fn(*args, **kwargs)
  File "/usr/local/lib64/python3.6/site-packages/django/core/management/commands/runserver.py", line 109, in inner_run
    autoreload.raise_last_exception()
  File "/usr/local/lib64/python3.6/site-packages/django/utils/autoreload.py", line 77, in raise_last_exception
    raise _exception[1]
  File "/usr/local/lib64/python3.6/site-packages/django/core/management/__init__.py", line 337, in execute
    autoreload.check_errors(django.setup)()
  File "/usr/local/lib64/python3.6/site-packages/django/utils/autoreload.py", line 54, in wrapper
    fn(*args, **kwargs)
  File "/usr/local/lib64/python3.6/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/usr/local/lib64/python3.6/site-packages/django/apps/registry.py", line 114, in populate
    app_config.import_models()
  File "/usr/local/lib64/python3.6/site-packages/django/apps/config.py", line 211, in import_models
    self.models_module = import_module(models_module_name)
  File "/usr/lib64/python3.6/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 994, in _gcd_import
  File "<frozen importlib._bootstrap>", line 971, in _find_and_load
  File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 678, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/usr/local/lib64/python3.6/site-packages/django/contrib/auth/models.py", line 2, in <module>
    from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
  File "/usr/local/lib64/python3.6/site-packages/django/contrib/auth/base_user.py", line 47, in <module>
    class AbstractBaseUser(models.Model):
  File "/usr/local/lib64/python3.6/site-packages/django/db/models/base.py", line 117, in __new__
    new_class.add_to_class('_meta', Options(meta, app_label))
  File "/usr/local/lib64/python3.6/site-packages/django/db/models/base.py", line 321, in add_to_class
    value.contribute_to_class(cls, name)
  File "/usr/local/lib64/python3.6/site-packages/django/db/models/options.py", line 204, in contribute_to_class
    self.db_table = truncate_name(self.db_table, connection.ops.max_name_length())
  File "/usr/local/lib64/python3.6/site-packages/django/db/__init__.py", line 28, in __getattr__
    return getattr(connections[DEFAULT_DB_ALIAS], item)
  File "/usr/local/lib64/python3.6/site-packages/django/db/utils.py", line 201, in __getitem__
    backend = load_backend(db['ENGINE'])
  File "/usr/local/lib64/python3.6/site-packages/django/db/utils.py", line 110, in load_backend
    return import_module('%s.base' % backend_name)
  File "/usr/lib64/python3.6/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/mysql/base.py", line 20, in <module>
    ) from err
django.core.exceptions.ImproperlyConfigured: Error loading MySQLdb module.
Did you install mysqlclient?

このケースでは丁寧に必要な措置が書かれているのでmysqlclientをインストールします。

Databaseエラー

runserverを実行後に下記のようなエラーが発生します。

$ python3 manage.py runserver 0:8000
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
Exception in thread django-main-thread:
Traceback (most recent call last):
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/base/base.py", line 217, in ensure_connection
    self.connect()
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/base/base.py", line 195, in connect
    self.connection = self.get_new_connection(conn_params)
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/mysql/base.py", line 227, in get_new_connection
    return Database.connect(**conn_params)
  File "/usr/local/lib64/python3.6/site-packages/MySQLdb/__init__.py", line 84, in Connect
    return Connection(*args, **kwargs)
  File "/usr/local/lib64/python3.6/site-packages/MySQLdb/connections.py", line 164, in __init__
    super(Connection, self).__init__(*args, **kwargs2)
MySQLdb._exceptions.OperationalError: (1045, "Access denied for user 'django'@'localhost' (using password: YES)")

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/lib64/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/usr/lib64/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib64/python3.6/site-packages/django/utils/autoreload.py", line 54, in wrapper
    fn(*args, **kwargs)
  File "/usr/local/lib64/python3.6/site-packages/django/core/management/commands/runserver.py", line 120, in inner_run
    self.check_migrations()
  File "/usr/local/lib64/python3.6/site-packages/django/core/management/base.py", line 453, in check_migrations
    executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
  File "/usr/local/lib64/python3.6/site-packages/django/db/migrations/executor.py", line 18, in __init__
    self.loader = MigrationLoader(self.connection)
  File "/usr/local/lib64/python3.6/site-packages/django/db/migrations/loader.py", line 49, in __init__
    self.build_graph()
  File "/usr/local/lib64/python3.6/site-packages/django/db/migrations/loader.py", line 212, in build_graph
    self.applied_migrations = recorder.applied_migrations()
  File "/usr/local/lib64/python3.6/site-packages/django/db/migrations/recorder.py", line 73, in applied_migrations
    if self.has_table():
  File "/usr/local/lib64/python3.6/site-packages/django/db/migrations/recorder.py", line 56, in has_table
    return self.Migration._meta.db_table in self.connection.introspection.table_names(self.connection.cursor())
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/base/base.py", line 256, in cursor
    return self._cursor()
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/base/base.py", line 233, in _cursor
    self.ensure_connection()
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/base/base.py", line 217, in ensure_connection
    self.connect()
  File "/usr/local/lib64/python3.6/site-packages/django/db/utils.py", line 89, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/base/base.py", line 217, in ensure_connection
    self.connect()
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/base/base.py", line 195, in connect
    self.connection = self.get_new_connection(conn_params)
  File "/usr/local/lib64/python3.6/site-packages/django/db/backends/mysql/base.py", line 227, in get_new_connection
    return Database.connect(**conn_params)
  File "/usr/local/lib64/python3.6/site-packages/MySQLdb/__init__.py", line 84, in Connect
    return Connection(*args, **kwargs)
  File "/usr/local/lib64/python3.6/site-packages/MySQLdb/connections.py", line 164, in __init__
    super(Connection, self).__init__(*args, **kwargs2)
django.db.utils.OperationalError: (1045, "Access denied for user 'django'@'localhost' (using password: YES)")

ユーザが存在しないかパスワードが間違っているため、データベースに接続できないエラーです。設定ファイルを確認してください。

サーバにつながらない

runserverを実行した後にブラウザから接続できない場合は以下の通りに切り分けてください。 まず、ネットワーク上の別PCからpingを実行します。

$ ping 192.168.12.9 -c 4
PING 192.168.12.9 (192.168.12.9) 56(84) bytes of data.
64 bytes from 192.168.12.9: icmp_seq=1 ttl=62 time=1.43 ms
64 bytes from 192.168.12.9: icmp_seq=2 ttl=62 time=1.75 ms
64 bytes from 192.168.12.9: icmp_seq=3 ttl=62 time=1.88 ms
64 bytes from 192.168.12.9: icmp_seq=4 ttl=62 time=1.90 ms

--- 192.168.12.9 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 1.431/1.744/1.904/0.189 ms

サーバから応答がない場合はネットワークの設定ミスの可能性があります。 /etc/sysconfig/network-scripts/などの設定ファイルを確認してみてください。

ping応答がある場合は続けてポートスキャンをします。

$ nmap 192.168.12.9 -Pn

Starting Nmap 6.40 ( http://nmap.org ) at 2019-08-03 23:20 JST
Nmap scan report for 192.168.12.9
Host is up (0.89s latency).
Not shown: 998 filtered ports
PORT     STATE SERVICE
22/tcp   open  ssh
8000/tcp open  http-alt

Nmap done: 1 IP address (1 host up) scanned in 58.85 seconds

正しくサーバが起動しているときは8000が表示されます。 されない場合はDjangoを起動しているサーバへログインしてネットワーク状態を確認します。

$ ss -nl | grep 8000
tcp    LISTEN     0      10        *:8000                  *:*

LISTENが表示されているばあはファイアウォールの設定を見直してください。 されていない場合は、Djangoの設定を再度確認してください。

*1:コマンドは"#"がrootで実行、"$"が一般ユーザで実行を表しています。状況に応じてsu -もしくはsudoでrootとして実行する必要があります。

Intel NUCとVMware ESXi 6.5上にSophos XG Firewallを構築する

仮想化技術の勉強のため新しくIntel NUCを購入しました。業務用サーバのようなデカくてうるさいやつはさすがに自宅に置けないので、省スペースなNUCを選びました。

この機種を選定した理由として、Core i5の4コアHT対応で論理コアが8コアあるからです。仮想環境で動かすホスト数によってはCPUリソースが足りなくなるかもしれないと考えたためです。

また、ゲストとしてSophos XG Firewall Home Editionを導入し、ネットワーク機器として運用します。このFWの最低動作条件以下の通りです。

  • x86064 (64 bit) CPU
  • 2 Network Card Interface (NIC)
  • 2GB RAM
  • 10GB HDD/SDD

動作させるにはNICが2つ必要となります。

続きを読む

第一級無線陸上技術士の受験記

 平成30年度の第一級陸上無線技術士試験に合格し、免状(一陸技)を得ることができました。 私は前回の記事電気通信主任技術者(伝送交換)を持っていましたので、「無線工学の基礎 」と「無線工学A 」は免除となります。

続きを読む

ZabbixのMapからteratermで自動ログインする

ネットワーク機器も増えてきたこともあり、監視する必要がでてきたためにZabbixを使っています。MAP画面を使うと装置のつながりが一目で分かります。

f:id:jianlan:20190206002145j:plain
zabbix map
普段はWindowsTeratermを使っているので、マップから起動してログインできると便利です。ということで、ブラウザからローカルのexeファイルを実行する方法を調べたのですが、セキュリティの観点から禁止されているようです。Internet ExplorerであればActiveXを使うことでJavaScriptから呼び出せるようですが、IEはサポートされなくなるため別の方法を考えます。

ブラウザから次のようにプロトコルを指定したリンクを使うことにより特定のプログラムを実行させることができます。

telnet://localhost/

この例ではtelnet接続のためのクライアントが起動し、localhostへ接続しようとします。どのプログラムが起動するかはレジストリに記述されています。regeditを使って下記のレジストリを確認します。

[HKEY_CLASSES_ROOT\telnet\shell\open\command]
(規定)="C:\Windows\System32\rundll32.exe" "C:\Windows\System32\url.dll",TelnetProtocolHandler %l

この規定の値を変更してみます。

[HKEY_CLASSES_ROOT\telnet\shell\open\command]
(規定)="C:\Python\Python35\python.exe" "C:\usr\script\show_args.py" %l

なお、show_args.pyは次のようになっています。

import sys
import msvcrt

print('# of arguments:', len(sys.argv))
print('arguments:', str(sys.argv))

msvcrt.getch()

telnetのリンクをクリックすると次のように表示されます。

# of arguments: 2
arguments: ['C:\\usr\\script\\show_args.py', 'telnet://localhost/']

引数としてエンコードされたURL文字列自身が渡されます。したがって、次のように処理をします。

  1. zabbixのマップからtelnetリンクを生成
  2. telnetリンクをクリック
  3. レジストリに登録されたプログラムを起動
  4. URLエンコードされた文字列を処理
  5. teratermマクロでログイン処理を実行する

まずは、マップのURLsにリンクを張ります。

f:id:jianlan:20190210204138p:plain
Zabbix Map Edit element

URLsのNameとURLを設定します。URLはCGIIPアドレスとユーザ名を渡します。今回は次のようにします。

http://localhost/macro/login.cgi?type=≪実行するマクロ名≫&ip=≪ip アドレス≫&args=≪ユーザ名≫
---
http://localhost/macro/login.cgi?type=ssh_login&ip=192.168.11.10&args=jinglan

参照先のCGIは以下のようになっています。

#!/usr/bin/perl
use strict;
use warnings;
use CGI;

# クエリパラメータ取得
my $query = new CGI;
my $telnet_url = 'localhost/'.$query->param('type').'.ttl '.$query->param('ip').' '.$query->param('args');

# レスポンスヘッダの出力
print "Content-type: text/html", "\n\n";

# HTMLの出力
my $buff =<<"EOM";
<!DOCTYPE html> 
<html>
<head>
  <meta http-equiv="Pragma" content="no-cache">
  <meta http-equiv="Cache-Control" content="no-cache">
  <script language="javascript">
    function main() 
    {
       document.location.href = "telnet://$telnet_url";
       //history.go(-1);  //Chromeだとうまく動作しないのでコメントアウト
    }
  </script>
  <title>Login</title>
</head>
<body onload="main()"></body>
</html>
EOM

print $buff;

このスクリプトはHTMLページを作成し、telnet://のアドレスへ自動的にリダイレクトします。これにより、レジストリに登録されたプログラムが実行されます。レジストリへの登録は下記の内容を.regファイルにするとダブルクリックで変更できるので簡単です。

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\telnet\shell\open\command]
@="\"C:\\Program Files (x86)\\teraterm\\ttpmacro.exe\" C:\\usr\\script\\login.ttl %l"

telnetプロトコルteratermマクロに関連付け、C:\usr\script\login.ttlを実行しています。login.ttlの内容は以下のようになっています。

arg_str = param2
strreplace arg_str 1 'telnet://localhost/' ''
do
  strreplace arg_str 1 '%20' ' '
loop while result = 1
strsplit arg_str ' '

SSH_CONN_PARAM = ""
strconcat SSH_CONN_PARAM groupmatchstr3
strconcat SSH_CONN_PARAM "@"
strconcat SSH_CONN_PARAM groupmatchstr2

include "Login_ManagementServer.ttl"
sendln "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " SSH_CONN_PARAM

ブラウザによるURLエンコードでスペースが%20に変換されるため、置換で戻しています。また、telnet://localhost/は不要なので削除しています。踏み台サーバ(Linux)にログインした後、sshコマンドで接続しています。

以上でMAPからワンクリックでパスワード入力まで自動化させることができました。注意点として、Windowstelnetプロトコルの処理を変更しているのでセキュリティには留意する必要があります。

電気通信主任技術者の受験記

昨年は電気通信主任技術者(伝送交換)を受験し、合格したのでそのときのメモです。

受験時にはすでに下記の資格を持っていました。

工事担任者(総合種)を持っていたので「システム」「設備」「専門」「法規」のうち「システム」は免除となりました。また、 基本情報処理技術者をもっているので「専門」はデータ通信を選択しました。

「設備」「法規」の試験対策として次の過去問を買いました。資格勉強では過去問は必須です。

過去問は多くの問題が収録されているものが良いです。調べたところこの本が一番収録されていました。「専門」を除く「システム」「設備」「法規」が収録されているので科目ごとに過去問を買う必要もありません。

「専門」については公式サイトの過去問4回分を使いました。この過去問は解説がないので自力で解答の理由を導く必要があります。しかし、 基本情報処理技術者試験を受けていればおおよそ解答の見当が付きます。基本情報処理技術者試験に出題されないような通信系の問題はルータなどの通信機器を扱った経験があれば難しくないでしょう。

 

 また、参考書は今回購入しませんでした。

「設備」に関しては工事担任者で電話機の通信方式(DPやISDN)、無線や伝送時の符号化について、基本情報処理技術者でMTBFや情報セキュリティといったトピックについて理解しているのであれば参考書は不要だと思います。自分の周りではオーム社のテキストを使っている人が多かったです。

「法規」に関しては丸暗記です。過去問を1周すればどんな問題が出題されるか、どれが正解なのか分かるようになります。法律用語や解釈に不慣れであれば参考書を購入するのも一考です。

電気通信主任技術者試験 これなら受かる 法規(改訂3版)

電気通信主任技術者試験 これなら受かる 法規(改訂3版)

続きを読む