PythonでExcelのセル入力&編集したExcelブックをDjangoでダウンロード(openpyxl)

はじめに

openpyxlを使ってPythonExcelファイルを編集します。

$ pip install openpyxl

Excelのセルを入力するスクリプト

以下のスクリプトでは次の単純なセル入力を実装してみます。

  • example.xlsx を開く
  • ワークシートを選択
  • 指定したセルに値を設定
  • ブックを上書き保存
import openpyxl

# 編集するExcelブックのパス
path_to_excel_book = 'example.xlsx'

# ブックを開く
wb = openpyxl.load_workbook(path_to_excel_book)

# ワークシートを選択
ws = wb.active

# A1形式でセル編集
ws['A1'] = 'foo'
ws['B3'] = 'bar'

# R1C1形式でセル編集
ws.cell(row=5, column=1).value = 'foo'
ws.cell(row=5, column=2).value = 'bar'
ws.cell(row=6, column=3).value = 'foo'
ws.cell(row=6, column=4).value = 'bar'
ws.cell(row=7, column=5).value = 'foo'
ws.cell(row=7, column=6).value = 'bar'

# ブックを上書き保存
wb.save(path_to_excel_book)

たったこれだけです。
実装したスクリプトを実行すると以下のようにExcelシートに値が入力されます。
以下の実行結果では新規作成したファイルを使っていますが、事前にExcelシートに設定しておいたフォーマットは維持されます。
実際のユースケースとしては、テンプレートとなる体裁を整えたファイルを事前に用意しておき、 DBやファイル等から取得した値をセルに設定することになるでしょう。

f:id:hiroki-sawano:20200806210319p:plain

特に説明していませんが、openpyxlを使えばブックやシートを作成したり、行を挿入したり、図を作成したりできるので、大抵の手入力作業は自動化できると思います。

Djangoのビューで編集したExcelファイルをダウンロード

openpyxl.writer.excel.save_virtual_workbookでメモリ上のワークブックを返却できますので、前述したようなスクリプトで加工したブックをダウンロードする機能を簡単に実装できます。

from django.http import HttpResponse
from openpyxl.writer.excel import save_virtual_workbook
...
def download(request):
    # Excelブックの編集処理
    wb = openpyxl.load_workbook(...)
    # ...
    response = HttpResponse(content=save_virtual_workbook(wb), content_type='application/vnd.ms-excel')
    response['Content-Disposition'] = 'attachment; filename="your_book.xlsx"'
    return response

PostgreSQLの外部データラッパー(FDW)でMongoDBにアクセス

はじめに

PostgreSQLには外部データラッパー(以降、FDW)と呼ばれる拡張機能があります。この拡張を使うとPostgreSQLから外部のデータソース*1に接続することができます。

本エントリでは、このFDWを用いてMongoDBのドキュメントをPostgreSQL上で扱う方法を説明します。

f:id:hiroki-sawano:20200804205803p:plain

MongoDBサーバ

まずはPostgreSQLから接続するMongoDBを用意します。
使用するバージョンは v4.0.8 です。

# mongo --version
MongoDB shell version v4.0.8

テストデータの用意

PostgreSQLから取得するデータを用意します。
適当に mydb データベースの mycollection コレクションに3つのドキュメントを登録しておきます。

> use mydb
> db.mycollection.insert({ foo: "1", bar: "2", foobar: "3"})
> db.mycollection.insert({ foo: "4", bar: "5", foobar: "6"})
> db.mycollection.insert({ foo: "7", bar: "8", foobar: "9"})
> db.mycollection.find().pretty()
{
    "_id" : ObjectId("5f2941a85ee8845a53018f66"),
    "foo" : "1",
    "bar" : "2",
    "foobar" : "3"
}
{
    "_id" : ObjectId("5f2941b95ee8845a53018f67"),
    "foo" : "4",
    "bar" : "5",
    "foobar" : "6"
}
{
    "_id" : ObjectId("5f2941c45ee8845a53018f68"),
    "foo" : "7",
    "bar" : "8",
    "foobar" : "9"
}
> 

PostgreSQLサーバ

PostgreSQLはバージョン9.6を使います。FDWが実装されている9.1以降であれば問題ないと思います。

$ psql --version
psql (PostgreSQL) 9.6.18

FDWのインストールでPython2系が必要になるので、Python2.7をインストールしておきます(インストール手順は省略)。

$ python --version
Python 2.7.5

Multicornのインストール

後述のFDWはMulticornで実装されているのでインストールします。
バージョンは動作確認した v1.4.0 を指定しています。

$ git clone git://github.com/Kozea/Multicorn.git -b v1.4.0
$ cd Multicorn
$ make && make install

yam_fdwのインストール

MongoDB用のFDWであるyam_fdwをインストールします。

$ git clone https://github.com/asya999/yam_fdw.git
$ cd yam_fdw
$ python setup.py install

外部テーブルの作成

以下のSQLで適当なデータベースを作成し、FDWを利用するための準備をします。

CREATE DATABASE mydb;
\c mydb
CREATE EXTENSION multicorn;
CREATE SERVER mongodb_mydb_server FOREIGN DATA WRAPPER multicorn OPTIONS (
  wrapper 'yam_fdw.Yamfdw'
);

続けて、MongoDBで作成したおいた mycollection に対応するテーブルを作成します。
_id 列は必須です。その他の列は参照したい項目を適切なデータ型で定義します。
userpassword にはMongoDBへの接続で認証に使用するユーザ情報を指定してください(認証なしの場合は省略)。

CREATE FOREIGN TABLE mycollection (
  "_id" VARCHAR NOT NULL,
  "foo" VARCHAR NOT NULL,
  "bar" VARCHAR,
  "foobar" INTEGER
) SERVER mongodb_mydb_server OPTIONS (
  host '<mongodb-host>',
  port '27017',
  db 'mydb',
  user '<username>',
  password '<password>',
  collection 'mycollection'
);

これで外部テーブル mycollection ができました。

# \d mycollection

         Foreign table "public.mycollection"
 Column |       Type        | Modifiers | FDW Options 
--------+-------------------+-----------+-------------
 _id    | character varying | not null  | 
 foo    | character varying | not null  | 
 bar    | character varying |           | 
 foobar | integer           |           | 
Server: mongodb_mydb_server
FDW Options: (host '...', port '27017', db 'mydb', collection 'mycollection')

MongoDBにアクセス

実際にSQLmycollection テーブルに対して実行すると、 以下のようにMongoDBに事前登録した mycollection のデータ3件を取得できます。

# SELECT * FROM mycollection;

           _id            | foo | bar | foobar 
--------------------------+-----+-----+--------
 5f2941a85ee8845a53018f66 | 1   | 2   |      3
 5f2941b95ee8845a53018f67 | 4   | 5   |      6
 5f2941c45ee8845a53018f68 | 7   | 8   |      9
(3 rows)

ドラクエタクトで再戦を自動化

はじめに

最近ドラクエタクトにハマってます。
スタミナ消費のないバトルロードでレベル上げしていますが、バトル後に 再戦 ボタンを押すのが面倒なので自動化します。

f:id:hiroki-sawano:20200802221151j:plain

注意事項

ゲームの自動化は不正利用の扱いを受けることもありますし、何よりつまらなくなるので、本エントリで紹介する方法を試す場合は自己責任でお願いします。

前提

iOS10以降

方法

知っている人は当たり前に使っている方法だと思いますが、iOSに標準搭載されているスイッチコントロールを使います。

  • Settings > Accessibility > Switch ControlRecipes を選択

f:id:hiroki-sawano:20200802214924p:plain

  • Create New Recipe... を選択

f:id:hiroki-sawano:20200802215112p:plain

  • 適当な名前を入力して、 Assign a Switch... を選択

f:id:hiroki-sawano:20200802215142p:plain

  • Full Screen を選択

f:id:hiroki-sawano:20200802215253p:plain

  • Custom Gesture を選択

f:id:hiroki-sawano:20200802215319p:plain

  • 再戦 ボタンの位置をタップ

f:id:hiroki-sawano:20200802215427p:plain

  • 少し時間をおいて同じ場所をもう一度タップした後に Save

タップ操作を繰り返すため、2回タップが必要です。

f:id:hiroki-sawano:20200802215502p:plain

  • Settings > Accessibility > Switch Control > RecipesLaunch Recipe で作成したレシピを選択

f:id:hiroki-sawano:20200802215609p:plain

  • Settings > Accessibility > Accessibility ShortcutSwitch Control を選択

ロックボタンのトリプルクリックでレシプを呼び出せるようにします。
ホームボタンのある機種だとホームボタンをトリプルタップになると思います。

f:id:hiroki-sawano:20200802215744p:plain

使ってみる

再戦 が表示される画面でロックボタンをトリプルクリックしてレシピを起動します。
画面タップをしたときに 再戦 ボタンの上にマーカーが表示され、再戦できれば成功です。
あとは画面上を適当に連続タップしておけば、指定の位置を一定の間隔でタップし続けてくれるので、延々とレベル上げしてくれます(オートバトルは有効化しておいてください)。
上限レベルで成長が止まると意味がないので、パーティーメンバのランクアップも可能な限り済ませておいた方が良いです。

f:id:hiroki-sawano:20200802220010p:plain

AnsibleでUbuntu18.04にDjangoアプリをデプロイ(Apache+PostgreSQL+Miniconda)

はじめに

Ansibleを使ってUbuntu18.04にDjangoアプリをデプロイしてみました。
作成したプレイブックの動作確認にはEC2を使用しました。
構築するシステムの構成は下図の通りです。 CondaでDjangoの実行環境を管理し、DBにはCondaでインストールしたPostgreSQL、WebサーバはApache(+mod_wsgi)を使います。

f:id:hiroki-sawano:20200801022123p:plain

Ansibleのインストール

pipでAnsibleをインストールします。また、ユーザ作成時に使用する password_hash で必要となる passlib もインストールします。

$ pip install ansible
$ pip install passlib # パスワードのハッシュ化に必要

ディレクトリ構成

site.yml がシステムの構成を定義したプレイブックです。
通常は色々とファイルを分けるべきですが、今回はシンプルさ優先で一つのファイルに全て詰め込んでます。
example がデプロイするDjangoのプロジェクト、 example/environments.yml が実行環境を定義したConda環境の設定ファイルです。

site.yml
example
├── manage.py
├── example
│  ├── __init__.py
│  ├── urls.py
│  ├── asgi.py
│  ├── wsgi.py
│  └── settings.py
├── environments.yml

用意したDjangoプロジェクトはほとんど django-admin startproject example で生成したままの状態ですが、 settings.py のみ、以下の記事に記載した方法でデータベースへの接続情報を管理するように手を加えています。

hiroki-sawano.hatenablog.com

今回動作環境のために用意したConda環境には最低限のパッケージのみインストールすることにします。

name: my_env
channels:
  - defaults
dependencies:
  - django=3.0.3
  - postgresql=11.2
  - psycopg2=2.7.6.1
  - python=3.7.3

作成したプレイブック

以下が作成したプレイブックです。各設定内容について後述します。
初プレイブックなので不備等あるかもしれません。

共通設定

1つのEC2インスタンスに対してコマンドを実行するだけなのでホストは全指定( hosts: all )にします。 ユーザはUbuntu AMIのデフォルトユーザで( user: ubuntu )、 root 権限へのエスカレーションを可能とします( become: yes )。

- hosts: all
  user: ubuntu
  become: yes

変数

vars に各種変数を指定します。用途は以下のコメントに記載の通りです。

  vars:
    user: "foo" # Djangoアプリを実行するユーザ
    password: "bar" # ユーザのパスワード
    pub_key: "<your-pub-key>" # SSHで使用する公開鍵
    conda_env_name: "my_env" # Conda環境の名前
    db_name: "my_db" # DBの名前
    db_user: "my_db_user" # DBのユーザ
    db_password: "my_db_password" # DBユーザのパスワード
    app_name: "example" # デプロイするアプリの名前
    app_src: "example" # ローカルにあるDjangoプロジェクトへのパス
    conda_yml_file: "example/environments.yml" # ローカルにあるCondaの環境設定ファイルへのパス
    home: "/home/{{ user }}" # ユーザのホーム(変更不要)
    conda_prefix: "{{ home }}/miniconda3" # Minicondaのインストール先
    conda_env: "{{ conda_prefix }}/envs/{{ conda_env_name }}" # Conda環境のパス(変更不要)
    project_dir: "{{ home }}/{{ app_name }}" # Djangoプロジェクトのデプロイ先
    project_package: "{{ project_dir }}/example" # wsgi.pyを持つプロジェクトパッケージ

環境変数

environment環境変数を指定します。 PostgreSQLPGDATADjangoDJANGO_SETTINGS_MODULE のみ指定しておきます。

  environment:
    PGDATA: "{{ project_dir }}/postgres_data"
    DJANGO_SETTINGS_MODULE: "example.settings"

タスク

以降に実装した各タスクと対応するコマンドイメージを記載します。
Ansibleの変数参照部分 {{...}} はコマンド例では ${...} と記述します。

aptパッケージのアップデート、アップグレード

aptを使います。
キャッシュのアップデートは1日( cache_valid_time: 86400 )です。
upgradeupdate_cache と同じく boolean かと思いきや文字列なのでクォーテーションで囲まないと警告が出ました。

- name: Update and upgrade apt packages
  apt:
    upgrade: "yes"
    update_cache: yes
    cache_valid_time: 86400

$ sudo apt update
$ sudo apt upgrade -y

タイムゾーンの設定

timezoneタイムゾーンAsia/Tokyo に設定します。

- name: Set timezone to Asia/Tokyo
  timezone:
    name: Asia/Tokyo

$ sudo timedatectl set-timezone Asia/Tokyo

ユーザの追加

user{{ user }} を作成し、 sudo 権限を与えます。

- name: Add user and add it to sudo
  user:
    name: "{{ user }}"
    shell: /bin/bash
    state: present
    groups: sudo
    append: yes
    password: "{{ password | password_hash('sha512') }}"

$ sudo adduser ${user}
$ sudo gpasswd -a ${user} sudo
$ sudo su - ${user}

SSH接続の設定

authorized_key.ssh/authorized_keys を設定し、 {{ user }}SSH接続できるようにします。

- name: Set authorized key for newly created user
  authorized_key:
    user: "{{ user }}"
    state: present
    key: "{{ pub_key }}"

$ cd ~
$ mkdir .ssh
$ chmod 700 .ssh
$ touch .ssh/authorized_keys
$ chmod 600 .ssh/authorized_keys
$ echo ${pub_key} > ./.ssh/authorized_keys

Minicondaのインストール

get_urlでMinicondaのインストーラーをダウンロードします。

- name: Download Miniconda
  get_url:
    url: https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
    dest: "{{ home }}/miniconda3.sh"
    mode: '0755'

shellでMinicondaをインストールします。
スクリプトはバッチモード( -b オプション)で起動することで対話を抑止し、続けて conda init を別途実行しています。

- name: Install Miniconda
  shell: "bash {{ home }}/miniconda3.sh -b -p {{ conda_prefix }}"
  args:
    creates: "{{ conda_prefix }}"
  become: yes
  become_user: "{{ user }}"
- name: Init conda
  shell: "{{ conda_prefix }}/bin/conda init bash"
  args:
    executable: /bin/bash
  become: yes
  become_user: "{{ user }}"

$ wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
$ bash Miniconda3-latest-Linux-x86_64.sh -p ${conda_prefix}
...
Do you wish the installer to initialize Miniconda3
by running conda init? [yes|no]
[no] >>> yes

Djangoプロジェクトのアップロード

fileDjangoプロジェクトのインストール先ディレクトリを作成し、 copyでローカルのプロジェクトをアップロードします。

- name: Create project directory
  file:
    path: "{{ project_dir }}"
    state: directory
    owner: "{{ user }}"
    group: "{{ user }}"
    mode: '0755'
- name: Upload django project
  copy:
    src: "{{ app_src }}/"
    dest: "{{ project_dir }}/"
    owner: "{{ user }}"
    group: "{{ user }}"
    mode: '0644'
    force: no

$ scp -i /path/to/private/key -r ${app_src} ${user}@${ansible_host}:${project_dir}

Conda環境の作成

copyでCondaの環境設定ファイルをアップロードし、shellでConda環境を作成します。

- name: Upload environments.yml
  copy:
    src: "{{ conda_yml_file }}"
    dest: "{{ home }}/environments.yml"
    owner: "{{ user }}"
    group: "{{ user }}"
- name: Create conda environment
  shell: "{{ conda_prefix }}/bin/conda env create -f {{ home }}/environments.yml -n {{ conda_env_name }}"
  args:
    executable: /bin/bash
    creates: "{{ conda_env }}"
  become: yes
  become_user: "{{ user }}"

$ scp -i /path/to/private/key ${conda_yml_file} ${user}@${ansible_host}:${home}/environments.yml
...
$ conda env create -f ${home}/environments.yml -n ${conda_env_name}

機密情報を記述したJSONファイルの作成

copysecrets.json を作成します。
ファイルではなくテキストを直接指定する場合は src ではなく content を使用します。
SECRET_KEY の部分は本来はランダムな値を生成すべきです。

- name: Create secrets.json
  copy:
    content: '{
      "SECRET_KEY": "t43m32_02dr5popsxczu*_!qyo@d7g!+e*0*@3u#_%vil=)i(u",
      "DB_NAME": "{{ db_name }}",
      "DB_USER": "{{ db_user }}",
      "DB_PASSWORD": "{{ db_password }}",
      }'
    dest: "{{ project_dir }}/secrets.json"
    owner: "{{ user }}"
    group: "{{ user }}"

$ echo '{"SECRET_KEY": ... }' > ${project_dir}/secrets.json

データベースクラスタの作成

shellinitdb を呼び出し、データベースクラスタを作成します。

- name: Create database cluster
  shell: "{{ conda_env }}/bin/initdb"
  register: result
  args:
    executable: /bin/bash
    creates: "{{ project_dir }}/postgres_data"
  become: yes
  become_user: "{{ user }}"

$ initdb

PostgreSQLサービスの作成・起動

copyPostgreSQLサービスを作成します。
直接PostgreSQLをサーバにインストールする場合は自動的にユニットファイルが作成されますが、 今回はConda環境のPostgreSQLを使いたいので自前で作成します。

- name: Create postgres service
  copy:
    content:
      "[Unit]\n
      Description=PostgreSQL database server\n
      After=network.target\n\n
      [Service]\n
      Type=simple\n
      User={{ user }}\n
      ExecStart={{ conda_env }}/bin/postgres -D {{ project_dir }}/postgres_data\n
      ExecReload=/bin/kill -HUP $MAINPID\n
      KillMode=mixed\n
      KillSignal=SIGINT\n
      TimeoutSec=0\n\n
      [Install]\n
      WantedBy=multi-user.target\n"
    dest: "/etc/systemd/system/postgres.service"

systemdでサービスを起動し、自動起動を有効化します。

- name: Start and enable postgres service
  systemd:
    state: started
    name: postgres
    enabled: yes

$ cat << EOF > /etc/systemd/system/postgres.service
[Unit]
Description=PostgreSQL database server
...
WantedBy=multi-user.target
EOF

$ sudo systemctl daemon-reload
$ sudo systemctl start postgres
$ sudo systemctl enable postgres

Djangoの各種初期設定

shellでConda環境をアクティベートした後、DBのマイグレーションや静的ファイルの収集などの作業を実施します。
Conda環境を有効にした状態で複数のコマンドを実行したいので、 with_items を活用しています。
Adminユーザの名前、パスワードは本来変数化すべきです。

- name: Database settings & Collect static files
  shell: ". {{ conda_prefix }}/etc/profile.d/conda.sh && conda activate {{ conda_env_name }} && {{ item }}"
  args:
    executable: /bin/bash
    chdir: "{{ project_dir }}"
  with_items:
    - psql postgres -c "create role {{ db_user }} with login password '{{ db_password }}';"
    - createdb {{ db_name }} -O {{ db_user }}
    - python manage.py migrate
    - python manage.py collectstatic --noinput
    - echo "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@example.com', 'admin')" | python manage.py shell
  when: result is changed
  become: yes
  become_user: "{{ user }}"

$ conda activate ${conda_env_name}
$ cd ${project_dir}
$ psql postgres -c "create role ${db_user} with login password '${db_password}';"
$ createdb ${db_name} -O ${db_user}
$ python manage.py migrate
$ python manage.py collectstatic --noinput
$ echo "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@example.com', 'admin')" | python manage.py shell

Apacheのインストール

aptでApache2をインストールします。

- name: Install Apache
  apt:
    pkg:
    - apache2
    - apache2-dev

$ sudo apt-get install -y apache2 apache2-dev

mod_wsgiのインストール

unarchivemod_wsgiをダウンロード・展開します。

- name: Download mod_wsgi
  unarchive:
    src: https://github.com/GrahamDumpleton/mod_wsgi/archive/4.5.14.tar.gz
    dest: "{{ home }}"
    remote_src: yes
    owner: "{{ user }}"
    group: "{{ user }}"

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

- name: Make mod_wsgi
  shell: "{{ item }}"
  with_items:
    - "./configure --with-python={{ conda_env }}/bin/python"
    - "make"
  args:
    executable: /bin/bash
    chdir: "{{ home }}/mod_wsgi-4.5.14"
    creates: "/usr/lib/apache2/modules/mod_wsgi.so"
  become: yes
  become_user: "{{ user }}"
- name: Install mod_wsgi
  shell: "make install"
  args:
    executable: /bin/bash
    chdir: "{{ home }}/mod_wsgi-4.5.14"
    creates: "/usr/lib/apache2/modules/mod_wsgi.so"

$ cd ~
$ wget https://github.com/GrahamDumpleton/mod_wsgi/archive/4.5.14.tar.gz
$ tar -zxvf 4.5.14.tar.gz
$ cd mod_wsgi-4.5.14/
$ ./configure --with-python=${conda_env}/bin/python
$ make
$ sudo make install

サイトの設定

copyApacheのサイトを設定し、commandで有効化します。
最後にsystemdApacheを再起動します。

- name: Create Apache site
  copy:
    content:
      "LoadFile {{ conda_env }}/lib/libpython3.7m.so.1.0\n
      LoadModule wsgi_module /usr/lib/apache2/modules/mod_wsgi.so\n
      WSGIPythonHome {{ conda_env }}\n\n
      <VirtualHost *:80>\n
        ServerName {{ ansible_host }}\n
        WSGIDaemonProcess PROCESS_GROUP user={{ user }} group={{ user }} python-path={{ project_dir }}\n
        WSGIProcessGroup PROCESS_GROUP\n
        WSGIScriptAlias / {{ project_package }}/wsgi.py process-group=PROCESS_GROUP\n\n
        <Directory {{ project_package }}/>\n
          <Files wsgi.py>\n
            Require all granted\n
          </Files>\n
        </Directory>\n\n
        Alias /static/ {{ project_dir }}/static/\n
        <Directory {{ project_dir }}/static/>\n
          Require all granted\n
        </Directory>\n
        CustomLog  {{ project_dir }}/access_log common\n
        ErrorLog   {{ project_dir }}/error_log\n
      </VirtualHost>\n"
    dest: "/etc/apache2/sites-available/{{ app_name }}.conf"
- name: Disable default site
  command: a2dissite 000-default
- name: Enable django app site
  command: a2ensite {{ app_name }}
- name: Restart Apache
  systemd:
    state: restarted
    name: apache2

$ cat << EOF > /etc/apache2/sites-available/${app_name}.conf
LoadFile ${conda_env}/lib/libpython3.7m.so.1.0
LoadModule wsgi_module /usr/lib/apache2/modules/mod_wsgi.s
...
EOF

$ sudo a2dissite 000-default
$ sudo a2ensite ${app_name}
$ sudo systemctl restart apache2

動作確認

EC2インスタンスの起動

Ubuntu Server 18.04のAMIでインスタンスを立ち上げます。
IPアドレス13.231.182.215 です。

f:id:hiroki-sawano:20200730224706p:plain

f:id:hiroki-sawano:20200730224939p:plain

プレイブックの実行

起動したインスタンスには標準でPython3が搭載されているので、即プレイブックを実行できます。
IPアドレス-i オプションで直接指定します。

$ ansible-playbook --private-key /path/to/private/key -u ubuntu -i 13.231.182.215, site.yml
...
TASK [Install Apache] ************************************************************************************************
changed: [13.231.182.215]

TASK [Download mod_wsgi] *********************************************************************************************
changed: [13.231.182.215]

TASK [Make mod_wsgi] *************************************************************************************************
changed: [13.231.182.215] => (item=./configure --with-python=/home/foo/miniconda3/envs/my_env/bin/python)
changed: [13.231.182.215] => (item=make)

TASK [Install mod_wsgi] **********************************************************************************************
changed: [13.231.182.215]

TASK [Create Apache site] ********************************************************************************************
changed: [13.231.182.215]

TASK [Disable default site] ******************************************************************************************
changed: [13.231.182.215]

TASK [Enable django app site] ****************************************************************************************
changed: [13.231.182.215]

TASK [Restart Apache] ************************************************************************************************
changed: [13.231.182.215]

PLAY RECAP ***********************************************************************************************************
13.231.182.215             : ok=25   changed=24   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Djangoアプリにアクセス

何も実装していないのでAdminサイトにアクセスできることを確認することにします。
以下の通り http://13.231.182.215/admin にアクセスすると、無事ログイン画面が表示されました。

f:id:hiroki-sawano:20200730225805p:plain

さいごに

初めてAnsibleを触ってみましたが、思っていたよりも簡単に利用できて好印象でした。 ドキュメントも充実していて、今回試した範囲だとそれほど悩むこともなかったです。 対象ホストにPythonさえ入っていれば良いので導入の障壁も低いと思います。 今後本格的に利用を検討していきたいです。

WebページのスクリーンショットをRobot Framework(+SeleniumLibrary)で撮るスクリプト

はじめに

URLの一覧を入力に、Webページのスクリーンショットをとる作業をRobot Framework(+SeleniumLibrary)で自動化してみます。
Robot Frameworkを使ってみたかっただけなのであまり実用性は考えてませんが、UI変更の都度発生するドキュメントのメンテ作業などで役立つかもしれません(認証部分の追加は必要ですが)。

robotframework.org

前提

便利なDockerイメージがあるのでこちらを使わせてもらいます。

github.com

実装

スクリプト capture.sh で次の処理を実行します。

  1. URLの一覧を標準入力もしくは第一引数のファイルから取得
  2. ./tasks/tasks.robotスクリーンショットを撮るタスクを出力
  3. robot-framework コンテナでタスクを実行
  4. ./reports/*.pngスクリーンショットを出力(ファイル名は Get Title で取得したページのタイトル)

実行例

https://www.google.comスクリーンショットを撮ってみます。

f:id:hiroki-sawano:20200728070312g:plain

DjangoでDockerコンテナのログをテキストエリアに流し込む(Server-Sent Events)

はじめに

DjangoでDockerコンテナのログをテキストエリアに出力する方法をまとめます。

前提

Dockerコンテナの操作には docker-py を使います。

github.com

django==3.0.8
docker==4.2.0

ビューの実装

ビュー tail_log を実装します。
このビューは、URLパラメタ container_name で指定したコンテナのログを docker-pyContainer.logsで取得し、 StreamingHttpResponseで返します。

import time
from datetime import datetime

import docker
from django.http import StreamingHttpResponse


def tail_log(request, container_name):
    client = docker.from_env()
    container = client.containers.get(container_name)
    return StreamingHttpResponse(
        _stream_docker_logs(container),
        content_type='text/event-stream'
    )


def _stream_docker_logs(container):
    for line in container.logs(
            stream=True, since=datetime.utcfromtimestamp(time.time())):
        yield 'data: {}\n\n'.format(line.decode('utf-8'))
        time.sleep(0.1)

urls.py にはTemplateViewで画面を描画する index と、上記の views.tail_log を呼び出す tail_log を追記します。

from django.urls import path
from django.views.generic.base import TemplateView

from .views import tail_log

urlpatterns = [
    path('<str:container_name>', TemplateView.as_view(template_name='tail_docker_logs/index.html'), name='index'),
    path('docker/logs/<str:container_name>', tail_log, name='tail_log'),
]

テンプレートの実装

テキストエリア( console )にEventSource.onmessageで取得したDockerコンテナのログを出力します。
ログにHTML要素が含まれるような場合はエスケープしないと正しく表示されません。

<html>
  <head>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  </head>
  <body>
    <p>> docker logs -f {{ request.resolver_match.kwargs.container_name }}</p>
    <textarea id="console" rows="30" cols="100" readonly></textarea>
    <script>
    $(function() {
      let ES = new EventSource(
        "{% url 'tail_log' request.resolver_match.kwargs.container_name %}"
      );
      ES.onmessage = function(e) {
        $('#console').append(`${e.data}\n`);
        $('#console').scrollTop(
          $('#console')[0].scrollHeight - $('#console').height()
        );
      };
    });
    </script>
  </body>
</html>

実行例

コンテナ ab3c3b9660b0 のログを確認しています。

f:id:hiroki-sawano:20200726004326g:plain

さいごに

実装したコードは以下のリポジトリに置きました。

github.com