Python:ランダムな文字列を作る

メモ。

UUID を生成。

>>> import uuid
>>> u = uuid.uuid4()
>>> u
UUID('b7206123-4b26-49ca-b33a-d0fb240c17f9')

Base32エンコードする。

>>> import base64
>>> b = base64.b32encode(u.bytes)
>>> b
b'W4QGCI2LEZE4VMZ22D5SIDAX7E======'

b はバイト文字列なので、文字列に変換。ついでに後ろに付いている = を取り去る。これは文字数合わせのためのパディングなので取ってしまって構わない。

>>> s = b.decode().rstrip('=')
>>> s
'W4QGCI2LEZE4VMZ22D5SIDAX7E'

さらに、見た目のランダムさを増すために、ラテン文字の大文字と小文字を混在させる。そのために、ランダムにラテン文字を小文字に変換する関数を定義。

>>> import random
>>> def down(c):
...     if random.choice([True, False]):
...         return c.lower()
...     else:
...         return c

これを、s の一文字ずつに適用。

>>> random_id = ''.join([ down(c) for c in s ])
>>> random_id
'W4qgCi2LEzE4vMz22D5sidAx7E'

これで OK。やや長いのが難点かな。

>>> len(random_id)
26

Ubuntu Server 22.04にSambaサーバをたてる(Dockerなしで)

Docker で Samba サーバをたてるのは諦めた。Ubuntu の上で直接 Samba サーバを動かすことにする。

まずはお決まりの apt update から。

takatoh@wplj:~$ sudo apt update

Samba サーバのインストール。

takatoh@wplj:~$ sudo apt install -y samba

設定ファイルを編集(バックアップをとってから)。

takatoh@wplj:~$ sudo cp /etc/samba/smb.conf /etc/samba/smb.conf.orig
takatoh@wplj:~$ sudo vim /etc/samba/smb.conf

ワークグループの名前を変更したほか、ユーザー認証なしでアクセスできる public と takatoh のみアクセスできる restricted の2つの共有フォルダを作った。

もとの設定ファイルとの差分:

takatoh@wplj:~$ diff /etc/samba/smb.conf.orig /etc/samba/smb.conf
29c29
<    workgroup = WORKGROUP
---
>    workgroup = PANICBLANKET
241a242,257
> 
> [public]
>     path = /mnt/data/samba
>     writable = yes
>     force create mode = 0644
>     force directory mode = 0755
>     guest ok = yes
>     guest only = yes
> 
> [restricted]
>     path = /mnt/data/samba_takatoh
>     writable = yes
>     force create mode = 0644
>     force directory mode = 0755
>     valid users = takatoh
>     force user = takatoh

コメントを除く全体はこう:

[global]
   workgroup = PANICBLANKET
   server string = %h server (Samba, Ubuntu)
   log file = /var/log/samba/log.%m
   max log size = 1000
   logging = file
   panic action = /usr/share/samba/panic-action %d
   server role = standalone server
   obey pam restrictions = yes
   unix password sync = yes
   passwd program = /usr/bin/passwd %u
   passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .
   pam password change = yes
   map to guest = bad user
   usershare allow guests = yes

[printers]
   comment = All Printers
   browseable = no
   path = /var/spool/samba
   printable = yes
   guest ok = no
   read only = yes
   create mask = 0700

[print$]
   comment = Printer Drivers
   path = /var/lib/samba/printers
   browseable = yes
   read only = yes
   guest ok = no

[public]
    path = /mnt/data/samba
    writable = yes
    force create mode = 0644
    force directory mode = 0755
    guest ok = yes
    guest only = yes

[restricted]
    path = /mnt/data/samba_takatoh
    writable = yes
    force create mode = 0644
    force directory mode = 0755
    valid users = takatoh
    force user = takatoh

で、共有用のディレクトリを作る。

takatoh@wplj:~$ sudo mkdir -p /mnt/data/samba
takatoh@wplj:~$ sudo chown nobody:nogroup /mnt/data/samba
takatoh@wplj:~$ sudo mkdir -p /mnt/data/samba_takatoh
takatoh@wplj:~$ sudo chown takatoh:takatoh /mnt/data/samba_takatoh

結果としてこうなった。

takatoh@wplj:~$ ls -l /mnt/data
total 8
drwxr-xr-x 2 nobody  nogroup 4096 May  6 18:13 samba
drwxr-xr-x 3 takatoh takatoh 4096 May  6 18:13 samba_takatoh

そして、smbd をリスタート。

takatoh@wplj:~$ sudo systemctl restart smbd

Samba にユーザーを作る。pdbedit コマンド。

takatoh@wplj:~$ sudo pdbedit -a takatoh
new password:
retype new password:
Unix username:        takatoh
NT username:          
Account Flags:        [U          ]
User SID:             S-1-5-21-3828460484-3255695466-1925772709-1000
Primary Group SID:    S-1-5-21-3828460484-3255695466-1925772709-513
Full Name:            takatoh
Home Directory:       \\WPLJ\takatoh
HomeDir Drive:        
Logon Script:         
Profile Path:         \\WPLJ\takatoh\profile
Domain:               WPLJ
Account desc:         
Workstations:         
Munged dial:          
Logon time:           0
Logoff time:          Thu, 07 Feb 2036 00:06:39 JST
Kickoff time:         Thu, 07 Feb 2036 00:06:39 JST
Password last set:    Fri, 06 May 2022 18:12:32 JST
Password can change:  Fri, 06 May 2022 18:12:32 JST
Password must change: never
Last bad password   : 0
Bad password count  : 0
Logon hours         : FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

これでOK。Windows マシンからアクセスしてみると、ちゃんと \\wplj\public には認証無しで読み書きできる。\\wplj\restricted にはユーザー名とパスワードを要求され、正しく入力すれば読み書きできる。

無事完了。

[追記]

Samba のインストールと設定に際して、ファイアウォール関連は何もしていない。設定は次のようになっている。

takatoh@wplj:~$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere                  
137                        ALLOW       Anywhere                  
138                        ALLOW       Anywhere                  
139                        ALLOW       Anywhere                  
445                        ALLOW       Anywhere                  
22/tcp (v6)                ALLOW       Anywhere (v6)             
137 (v6)                   ALLOW       Anywhere (v6)             
138 (v6)                   ALLOW       Anywhere (v6)             
139 (v6)                   ALLOW       Anywhere (v6)             
445 (v6)                   ALLOW       Anywhere (v6)

Docker を使ってやってたときには何が悪かったのか、結局よくわからない。

DockerでSambaサーバをたてたけど外部からつながらない

昨日・今日といろいろ調べながら作業をしたけど、何が悪いのかわからない。もう Docker を使うのは諦めて、Ubuntu に直接 Samba サーバをインストールすることにしようと思うが、記録だけ残しておく。あ、「外部」ってのはローカルネットワーク上の別の PC のこと。

環境

ホストはこの間インストールした Ubuntu Server 22.04 だ。マシン自体は古いけど、いま Samba サーバになってるマシン(これは CentOS 7)はもっと古いので、これを置きかえるつもり。

  • Ubuntu Server 22.04 LTS
  • Docker version 20.10.14, build a224086349
  • docker-compose version 1.29.2, build unknown
  • dperson/samba (Samba の Docker イメージ)

Docker と docker-compose は Ubuntu のインストール時に一緒にインストールしたもの。Samba には公式の Docker イメージがないらしく、ググるとこの dperson/samba がよく使われてるようなのでこれにした。

Samba サーバをたてる

あちこちのページを参考にして、docker-compose.yml ファイルはこうした。

version: "3"

services:

  samba:
    image: dperson/samba
    container_name: samba
    restart: always
    ports:
      - "137:137"
      - "138:138"
      - "139:139"
      - "445:445"
    volumes:
      - "/mnt/data/samba:/mnt/public"
    command: [
      "-r",
      "-S",
      "-w", "PANICBLANKET",
      "-s", "public;/mnt/public;yes;no;yes;all"
    ]

これで Docker コンテナを起動してやる。

takatoh@wplj:~$ docker-compose up -d
Creating network "takatoh_default" with the default driver
Creating samba ... done
takatoh@wplj:~$ docker-compose ps
Name              Command                State                  Ports           
--------------------------------------------------------------------------------
samba   /sbin/tini -- /usr/bin/sam    Up (healthy)   0.0.0.0:137->137/tcp,:::137
        ...                                          ->137/tcp, 137/udp, 0.0.0.0
                                                     :138->138/tcp,:::138->138/t
                                                     cp, 138/udp, 0.0.0.0:139->1
                                                     39/tcp,:::139->139/tcp, 0.0
                                                     .0.0:445->445/tcp,:::445->4
                                                     45/tcp

次に、外部から接続できるようにホスト側のファイアウォールを設定する。Samba に関連するポートは、137、138、139、445 だ。ufw コマンドで接続を許可する。

akatoh@wplj:~$ sudo ufw allow 137
Rule added
Rule added (v6)
takatoh@wplj:~$ sudo ufw allow 138
Rule added
Rule added (v6)
takatoh@wplj:~$ sudo ufw allow 139
Rule added
Rule added (v6)
takatoh@wplj:~$ sudo ufw allow 445
Rule added
Rule added (v6)

そして、ファイアウォールを有効化。

takatoh@wplj:~$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

次のような状態になった。

akatoh@wplj:~$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere                  
137                        ALLOW       Anywhere                  
138                        ALLOW       Anywhere                  
139                        ALLOW       Anywhere                  
445                        ALLOW       Anywhere                  
22/tcp (v6)                ALLOW       Anywhere (v6)             
137 (v6)                   ALLOW       Anywhere (v6)             
138 (v6)                   ALLOW       Anywhere (v6)             
139 (v6)                   ALLOW       Anywhere (v6)             
445 (v6)                   ALLOW       Anywhere (v6)

これで完了(のはず)。

Windows から接続テスト

Samba サーバは出来上がったはずなので、Windows マシンから接続できるかテストしてみたところ、つながらない。共有フォルダどころか、コンピュータ(ホスト名は wplj)さえも見つからないと言われる。困った。

ネットワークの確認

Samba サーバをたてた wplj とは別の Ubuntu マシン(ホスト名は apostrophe)から、ネットワーク越しに接続できるのかを確認してみた。

まずは ping

takatoh@apostrophe:~$ ping wplj
PING wplj (192.168.2.12) 56(84) バイトのデータ
64 バイト応答 送信元 wplj (192.168.2.12): icmp_seq=1 ttl=64 時間=0.619ミリ秒
64 バイト応答 送信元 wplj (192.168.2.12): icmp_seq=2 ttl=64 時間=0.538ミリ秒
64 バイト応答 送信元 wplj (192.168.2.12): icmp_seq=3 ttl=64 時間=1.15ミリ秒
64 バイト応答 送信元 wplj (192.168.2.12): icmp_seq=4 ttl=64 時間=0.694ミリ秒
^C
--- wplj ping 統計 ---
送信パケット数 4, 受信パケット数 4, パケット損失 0%, 時間 3057ミリ秒
rtt 最小/平均/最大/mdev = 0.538/0.750/1.150/0.237ミリ秒

OK。そりゃそうだ、SSH で接続できてんだから。

なら、ポートが使えるかどうかを確認する nc コマンド。繋がるのがわかっている 22 番ポート(SSH)で試してみるとこうなる。

takatoh@apostrophe:~$ nc -vz wplj 22
Connection to wplj 22 port [tcp/ssh] succeeded!

では、Samba の使っている(はず)のポートではどうか。

akatoh@apostrophe:~$ nc -vz -u wplj 137
takatoh@apostrophe:~$ nc -vz -u wplj 138
takatoh@apostrophe:~$ nc -vz wplj 139
nc: connect to wplj port 139 (tcp) failed: Connection refused
takatoh@apostrophe:~$ nc -vz wplj 445
nc: connect to wplj port 445 (tcp) failed: Connection refused

137 番と 138 番で -u オプションを指定してるのは udp だから。何も出力されてないのは、接続できないからだ。tcp をつかう 139 番と 445 番は「失敗した」と明快に出力されてる。

一方のサーバ(wplj)側ではどうだろう。ss コマンドを使う。

takatoh@wplj:~$ ss -natu
Netid State  Recv-Q Send-Q                      Local Address:Port Peer Address:Port                                   Process                                  
udp   UNCONN 0      0                           127.0.0.53%lo:53        0.0.0.0:*                                                                               
udp   UNCONN 0      0                     192.168.2.12%enp1s0:68        0.0.0.0:*                                                                               
udp   UNCONN 0      0      [fe80::cad3:ffff:fe9d:2f5f]%enp1s0:546          [::]:*                                                                               
tcp   LISTEN 0      4096                        127.0.0.53%lo:53        0.0.0.0:*                                                                               
tcp   LISTEN 0      128                               0.0.0.0:22        0.0.0.0:*                                                                               
tcp   ESTAB  0      0                            192.168.2.12:22   192.168.2.14:58570                                                                           
tcp   LISTEN 0      128                                  [::]:22           [::]:*

Status の UNCONNLISTEN はそれぞれ udp、tcp の待受状態を表す。一つだけある ESTAB はつながっていることを表していて、これは SSH(22番ポート)だ。見事に Samba 関連のポートがない。

[追記]

いまためしたら、Samba のコンテナが起動してなかった。起動した上で ss コマンドを試すとこうなった。

takatoh@wplj:~$ ss -natu
Netid State  Recv-Q Send-Q                      Local Address:Port Peer Address:Port                                   Process                                  
udp   UNCONN 0      0                           127.0.0.53%lo:53        0.0.0.0:*                                                                               
udp   UNCONN 0      0                     192.168.2.12%enp1s0:68        0.0.0.0:*                                                                               
udp   UNCONN 0      0      [fe80::cad3:ffff:fe9d:2f5f]%enp1s0:546          [::]:*                                                                               
tcp   LISTEN 0      4096                              0.0.0.0:137       0.0.0.0:*                                                                               
tcp   LISTEN 0      4096                              0.0.0.0:138       0.0.0.0:*                                                                               
tcp   LISTEN 0      4096                              0.0.0.0:139       0.0.0.0:*                                                                               
tcp   LISTEN 0      4096                        127.0.0.53%lo:53        0.0.0.0:*                                                                               
tcp   LISTEN 0      128                               0.0.0.0:22        0.0.0.0:*                                                                               
tcp   LISTEN 0      4096                              0.0.0.0:445       0.0.0.0:*                                                                               
tcp   ESTAB  0      0                            192.168.2.12:22   192.168.2.14:58570                                                                           
tcp   LISTEN 0      4096                                 [::]:137          [::]:*                                                                               
tcp   LISTEN 0      4096                                 [::]:138          [::]:*                                                                               
tcp   LISTEN 0      4096                                 [::]:139          [::]:*                                                                               
tcp   LISTEN 0      128                                  [::]:22           [::]:*                                                                               
tcp   LISTEN 0      4096                                 [::]:445          [::]:*

Samba 関連のポートを待受けるようになった。けど、やっぱり他のコンピュータからはつながらない。

takatoh@apostrophe:~$ nc -u -vz wplj 137
takatoh@apostrophe:~$ nc -u -vz wplj 138
takatoh@apostrophe:~$ nc -vz wplj 139
nc: connect to wplj port 139 (tcp) failed: Connection timed out
takatoh@apostrophe:~$ nc -vz wplj 445
nc: connect to wplj port 445 (tcp) failed: Connection timed out

[追記ここまで]

まとめ

というわけで、どうやら Docker でたてた Samba のせいじゃなく、コンピュータ間で通信できてないように見える。SSH は問題なくつながってるのに何が違うんだ?

Ubuntu Server 22.04 LTSのインストール

先日リリースされたばかりの Ubuntu Server 22.04 LTS をインストールしてみた。インストール先は Rocky Linux をインストールしたまま使いみちを考えてなかった PC だ。これを、いまバックアップサーバになってる CentOS 7 のサーバと置き換えることにする。Windows PC からもアクセスするので Samba を入れる必要があるけど、それはまた今度。今日は Ubuntu のインストールだけ。

といっても、去年 Ubuntu Server 20.04 LTS をインストールしたときとやることは変わらないので、手順については以前の記事のリンクを置いておく。

違うところといえば、Featured Server Snaps の選択のところで docker のほかに aws-cli も選択してインストールしたことくらい。20.04 のときはなかった(か、気が付かなかった)。

というわけで、インストールは無事終了した。

だけどひとつだけやらかした。そういえば去年も同じところで引っかかったんだ。何かというと、インストール時に作ったユーザのパスワードだ。

日本語配列のキーボードを使ってるんだけど、Ubuntu Server のインストーラには日本語キーボードの選択肢がない。というか 20.04 LTS の時にはなかったんで今回も言語選択のところで English を選んだ(もしかしたら Japanese もあったのかもしれない。ないと思っていたので確認しなかった)。日本語と英語のキーボードだと、アルファベットや数字はいいけど、記号の配列が違うんだ。で、パスワードに記号を使ってしまうと、入力したつもりの文字と実際に入力された文字が違うことになる。

これが問題になるのは SSH でほかの PC から接続して sudo コマンドを使ったとき。「ほかの PC」ってのは Windows か Ubuntu Desktop で、日本語配列のキーボードが問題なく使えてる。でも、SSH 接続した先の Ubuntu Server で sudo コマンドにパスワードを要求されて入力すると、正しいパスワードを入力してるのにエラーになる。なぜかというと、Ubuntu Server 側には正しい(言い換えると、意図したとおりの)パスワードが設定されてないからだ。今回もこれで30分ぐらい無駄にした。

英語配列のキーボード、ひとつ買っとこうかな。

新しいWindows11マシン

メインじゃないほうの Windows マシンを新しくした。

本当は今年の後半まで待ちたかったんだけど(主に経済的な理由で)、Bluetooth のキーボードとマウスを認識してくれなくなってしまったので、前倒しして買い替えた。

届いたのは火曜日。今日やっと最低限のセットアップだけして、この記事を書いてる。アプリも Office と Dropbox しかインストールしてない。

ほかにやってるのは Ubuntu 22.04 LTS のダウンロード(まさに今ダウンロード中)。

なんとも内容のない記事だけど、いいんだ。書かないと今月ひとつも記事がないことにもなりかねないから。あ、でも Ubuntu のインストールをすればネタができるか。

Rocky Linuxをインストールしてみた

ローカルネットワーク上でサーバにしてた古いPCが空いたので、CentOS の代替のひとつ、Rocky Linux をインストールしてみた。

Rocky Linux の公式サイトはここ。

最新のバージョンは 8.5 で、EOL は 2029/3/31 となってる。ダウンロードページからはいくつかの種類の ISO イメージがダウンロードできるけど、DVD 版をダウンロードした。

ダウンロードした ISO イメージは USBメモリに焼いておく(この作業は Windows で)。

USB メモリから起動するとメニューが表示されるので、「Install Rocky Linux 8」を選択。インストーラは GUI で、日本語に対応してるし、マウスも使えるし、Ubuntu Server のインストールよりもやりやすい。基本的にはインストーラの指示に沿って進めていけばOK。主なところを書き出すと次の通り:

  • キーボード: 日本語
  • 言語サポート: 日本語
  • 時刻と日付: アジア/東京
  • rootのパスワード
  • ユーサーの作成: takatohを作成した
  • ソフトウェアの選択: サーバ(GUI使用)
  • Windowsファイルサーバーを追加
  • インストール先: 内蔵HDD
  • ストレージの設定: 自動設定
  • KDUMP: 有効(デフォルトのまま)
  • ホスト名: wplj

これでインストールを開始。しばらく時間がかかる。

インストールが終了したら再起動。初期セットアップの画面になるけどライセンスに同意すればいいだけ。

あらためてインストール時に作ったユーザー takatoh でログインすると、今度はユーザーの初期設定がはじまる。これもいくつか設定するだけで「使用する準備が完了しました。」となる。

別のマシンから ssh でログインできることも確認した。Samba もインストールされてるけど有効にはなってなかった。これは共有フォルダの設定とかが必要だし、そういうもんだろう。OS のインストール時についでにインストールできるだけでも楽だ。

というわけで、本格的に使うにはもっと手を入れる必要があるけど、インストール自体は簡単にできることがわかった。使ってみるかな。

Let’s EncryptとcertbotとDockerを使ってwebアプリをSSL化する

Let’s Encrypt はフリーで利用できる認証局だ。

今回はこの Let’s Encrypt を利用して、既存の web アプリを SSL 化(つまり、https://〜でアクセスできるように)してみた。

Let’s Encrypt を利用するには certbot というツールをインストールして、サーバ証明書の発行や更新ができる。だけど、webアプリやwebサーバは Docker のコンテナ上で動かしているので、せっかくだから certbot も Docker でできないかと調べてみたら、ちゃんとやり方があった。certbot は Docker Hub にイメージが公開されている。

以下、ウチのwebアプリはこのやり方でできたよ、という記録。

前提

  • 既存のwebアプリ(http://webapp.panicblanket.com/)をSSL化する
  • webアプリ、webサーバ(Nginx)は Docker コンテナ上で動いている
  • Docker と docker-compose がインストール済み

付け加えると、webアプリの / (ルート)は他のページへリダイレクトされている。ここが最初よくわからなかったところだ。

certbot を使うには、対象のサーバの / に Let’s Encrypt からアクセスできるようにしておく必要がある。というのも certbot はサーバ認証のために一時的なファイルを作って、Let’s Encrypt からアクセスできるのを確認することでサーバの存在を確認しているらしいからだ。だけど、webアプリでは他のページにリダイレクトしているので、うまく行かないんじゃないかと思っていた。

結論からいうと、webアプリのリバースプロキシになっているwebサーバをいったん停めて、認証取得用に別のwebサーバをたてることで対処することができた。

手順

大まかな手順は次の通り:

  1. 認証取得用の設定ファイルを書く
  2. 既存webアプリのリバースプロキシになってるwebサーバを停める
  3. certbot の Dockerコンテナを使って認証取得
  4. 既存のwebサーバの設定ファイルをSSL対応にして再起動

認証取得用の設定ファイル

Docker、というか docker-compose を使うので docker-compose.yml ファイルを書く。

ersion: "3"

services:
  nginx:
    image: nginx:1.19.2-alpine
    ports:
      - 80:80
    volumes:
      - ./conf.d:/etc/nginx/conf.d
      - ./html:/var/www/html
      - /etc/letsencrypt:/etc/letsencrypt
      - /var/lib/letsencrypt:/var/lib/letsencrypt

  certbot:
    image: certbot/certbot:latest
    volumes:
      - ./html:/var/www/html
      - /etc/letsencrypt:/etc/letsencrypt
      - /var/lib/letsencrypt:/var/lib/letsencrypt
    command: ["--version"]

Nginx と certbot のイメージを利用する。この Nginx は認証取得のための一時的なものだ。

で、その Nginx の設定ファイルはこう:

server {
    listen      80;
    listen      [::]:80;

    server_name webapp.panicblanket.com;

    root        /var/www/html;
}

80番ポートで待ち受けるだけ。このファイルを、docker-compose.yml ファイルからみて ./conf.d/webapp.panicblanket.com.conf に保存する。ドキュメントルートはディレクトリを作っておけば、何もなくて構わない。

認証取得

取得する前に既存のwebサーバを停めておくこと。

次に、認証用のwebサーバだけ起動する。

$ docker-compose up -d nginx

webサーバが起動したら、certbot を使って認証取得する。

$ docker-compose run --rm certbot certonly --webroot -w /var/www/html -d webapp.panicblanket.com

これで証明書が /etc/letsencrypt 以下に保存される。

確認できたら Nginx も停める。

$ docker-compose down

webアプリとサーバの設定、再起動

証明書が取得できたので、それを利用して https://〜 でアクセスできるように設定ファイルを書き換える。

まずは、Nginx のバーチャルホストの設定ファイル。

upstream webapp {
    server webapp:9000;
}

server {
    listen      80;
    listen      [::]:80;

    server_name webapp.panicblanket.com;

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen      443 ssl http2;
    listen      [::]:443 ssl http2;

    server_name webapp.panicblanket.com;

    ssl_certificate     /etc/letsencrypt/live/webapp.panicblanket.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/webapp.panicblanket.com/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_tickets off;

    ssl_protocols TLSv1.3 TLSv1.2;
    ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256';
    ssl_prefer_server_ciphers off;

    access_log /var/log/nginx/webapp.panicblanket.com.access.log main;
    error_log  /var/log/nginx/webapp.panicblanket.com.error.log warn;

    keepalive_timeout     60;
    proxy_connect_timeout 60;
    proxy_read_timeout    60;
    proxy_send_timeout    60;

    location / {
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_pass       http://webapp;
    }
}

80番ポートにアクセスしてきたらすべて https://~ にリダイレクトしている。

あと、docker-compose.yml(の該当部分)。

  httpserver:
    image: nginx:1.18.0
    container_name: httpserver
    restart: always
    ports:
      - 80:80
      - 443:443
    volumes:
      - /home/takatoh/docker-environment/nginx/etc:/etc/nginx/conf.d
      - /home/takatoh/docker-environment/nginx/log:/var/log/nginx
      - /etc/letsencrypt:/etc/letsencrypt

443番ポートと /etc/letsencrypt を追加。

これでコンテナを再起動すればOK。

$ docker-compose restart webapp httpserver

参考にしたページ

古いRailsアプリをDockerコンテナに乗せた

もう1年も前になるけどこんな記事を書いた。

ローカルネットワークのサーバ置き換えに際して、古いサーバ(Ubuntu 16.04 LTS)で動かしていた Rails アプリが、新しいサーバ(Ubuntu 20.04 LTS)上で動かせなくてどうしよう……っていう記事だ。

あれから1年も過ぎてしまったけど、今年に入ってから週末ごとにちまちまと作業をして、なんとか新しいサーバの Dockerコンテナで動かせるまでになった。

ベースにした Docker イメージは、Ruby の公式イメージのうち一番古いバージョン、2.6.9。とにかく Ruby の公式 Docker イメージをベースにしたコンテナで動かせることを目標にした。

Rails は 5.0.7.2まで上げた。これだって今となっては古いバージョン(RubyGems.org を見ると2019/3/13リリース)だけど、ローカルネットワークでしか使ってない web アプリなのでひとまず妥協する。

作業は開発用メインマシン(Ubuntu 20.04 LTS)の rbenv で Ruby そのもののバージョンも変えながらやった。作業を始めた時点では Rails 4.1.4 で、最後のコミットはなんと 2016/3/3 だった。6年近くもメンテしてなかったってことだよ。アプリを使う分には不自由してなかったからだけど、だからってほったらかしだとこうやって後になってツケが来るんだよな。Rails にさわるもの6年ぶりってわけで、少しずつバージョンを上げるたびに出るエラー(主に依存関係)で苦労した。これからはもう少し面倒を見て、追いつくようにしよう。

さて、そういうわけで、動かしていたアプリがなくなって、めでたく古いサーバが空いた。春になれば Ubuntu 22.04 LTS もリリースされるだろうから、そのテスト用にしようかな。

Windows11へのGNU Fortranのインストールと算術IFについて

古いFortran(FORTRAN77だ!)のプログラムをコンパイルする必要があって、GNU Fortran をインストールした。

GNU Fortran の公式サイトは↓ここにあるんだけど、どこからダウンロードすればいいんだか、ひどくわかりにくい。

さいわい、丁寧に説明してくれているページを見つけた。日付が古いが内容は古くなってない。

公式サイトのここをクリックして、次の画面のここをクリックして……みたいなことをグダグダと書かれているが――という言い方はよくないな。丁寧に説明してくれてるんだからありがたい。悪いのはわかりにくい公式サイトだ――要するに TDM GCC をインストールすれば GNU Fortran もインストールできる。TDM GCC のサイトは↓ここ。

バージョン10.3.0が最新だった。ダウンロードしたのは64ビット版のtdm64-gcc-10.3.0-2.exe。

このファイルは Windows のインストーラになってるので、普通にインストール作業すれば何も困ることはない。ただ、デフォルトでは Fortran はインストールされないので、途中で Fortran をインストールするようにチェックを入れる必要がある。

さて、インストールできたので早速試してみた。コマンドは gfortran

takatoh@sofa: sample > gfortran --version
GNU Fortran (tdm64-1) 10.3.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

コンパイルするには引数にソースファイルの名前を指定すればいい。-o オプションはコンパイルされた実行ファイルの名前の指定。省略すると a.exe になる。

takatoh@sofa: sample > gfortran -o auto auto.for

ソースファイルの拡張子が .for だと FORTRAN77 だと見做すらしい。.f90 なら Fortran90。

こんな調子で20個ほどコンパイルしたんだけど、そのひとつで次のような warning がでた。

takatoh@sofa: sample > gfortran -o ohsp ohsp.for
ohsp.for:70:72:

   70 |       IF(RLOG-R1) 170,180,180
      |                                                                        1
Warning: Fortran 2018 deleted feature: Arithmetic IF statement at (1)

Arithmetic IF statement ってなんだと思って調べたら、算術IFっていうんだと。

↓ここに説明がある。

条件式の値が負かゼロか正かで分岐して指定された番号の文へジャンプする。条件式は論理式じゃなくて算術式、つまり評価すると数値になる式。こんな感じ:

      IF(X-10) 100,200,300
  100 ...
      GO TO 500
  200 ...
      GO TO 500
  300 ...
      GO TO 500
  500 ...

X-10 が負なら100へ、ゼロなら200へ、正なら300へジャンプする。

もう少し調べてみたら、論理IF よりも 算術IF のほうが先にあったらしい。

FORTRAN77 って学生の時にやったんだけど、こんなの覚えてないよ。

それはさておき、こういう古いソースコードでもコンパイルできるってのはすごいことだよな。

Python: Larkが1.0.0になったので試してみたんだけど挙動がおかしい

年が明けたら Lark の 1.0.0 がリリースされていたのに気が付いたので、以前作ったコマンド形式の入力ファイルをパースするのを、再度やってみた。フレームワーク的なパッケージにまとめて再利用できるようにしてみようと思う……んだけど、どうも挙動がおかしい。

要点を先に書くと、書いたパーサも入力ファイルも同じなのに、実行するたびに結果が異なる。原因はわからない。パーサの書き方が悪いのか、Lark のバグなのか。

順を追って説明する。

コマンド形式の入力ファイル

プログラムには何らかの入力が必要で、入力ファイルにも何らかのフォーマットが必要だ。コマンド形式というのは、入力データの内部表現を、ひとつずつコマンドを実行しながら組み立てようというものだ。この記事で扱うのは、そのフレームワーク的なライブラリで、ファイルからの入力である文字列を、コマンド(と引数)の列にパースするもの。

サンプルとして次のような入力ファイルを使う。

TITLE "example"
ADD 1 2
MUL
    3
//    5
    7  8  
    9
// this line is comment.
REPEAT "Hello!" 5  
    // this is comment too.
JOIN "Hello," "world!"  // in-line comment.
ADD-2  5  .7e03
NO-ARG-COMMAND
COMMAND-WITH-KEYWORD
    KW1  1
    KW2  2
    KW3  on
    KW4  off

文法

全体を「スクリプト」と呼ぶ。文法は次の通り。

  • 「スクリプト」は1つ以上の「ステートメント」からなる
  • 「ステートメント」は「コマンド」と0個以上の「引数」からなり、空白文字で区切られ、改行文字で終わる
  • ただし、空白文字で始まる行は前の行の続きとみなす。なので「コマンド」は必ず行頭からはじまる
  • 「引数」には数値、文字列、キーワード、真偽値の4種類がある
  • //から行末まではコメント

「引数」について補足しておく。数値は文字通りの数値だけど、整数/実数の区別はなし。文字列はダブルークォートで囲む。真偽値は true / falseyes / noon / off のいずれか(すべて小文字)。

で、キーワードが今日の主題。「引数」のリストの中で、次の引数が何のデータかを示す目印に使う。要するに Python のメソッドのキーワード引数のような使い方だ。大文字のラテン文字と数字からなり、行頭には来ないことでコマンドと区別できる。実装としては Ruby なら Symbol をつかうところだけど、Python にはそういうのがないので(ないよね?)、Keyword クラスを定義した。

文法ファイルは次のようになった。

?script : statement+

statement : line continued*

line : command _WS_INLINE arglist _WS_INLINE? _NL
     | command _WS_INLINE? _NL

continued : _INDENT arglist _WS_INLINE? _NL

command : CMDNAME

CMDNAME : UCASE_LETTER ("-"|UCASE_LETTER|DIGIT)*

arglist : arg
        | arglist _WS_INLINE arg

arg : number
    | string
    | keyword
    | boolean

number : SIGNED_NUMBER

string : ESCAPED_STRING

keyword : KWORD

KWORD : UCASE_LETTER (UCASE_LETTER|DIGIT)*

boolean : true
        | false

true : "true" | "yes" | "on"

false : "false" | "no" | "off"

_WS_INLINE : WS_INLINE

_NL : NEWLINE

_INDENT : WS_INLINE


%import common.UCASE_LETTER
%import common.DIGIT
%import common.SIGNED_NUMBER
%import common.ESCAPED_STRING
%import common.NEWLINE
%import common.WS_INLINE
%import common.CPP_COMMENT

COMMENT : WS_INLINE? CPP_COMMENT NEWLINE
COMMENT_INLINE : WS_INLINE? CPP_COMMENT

%ignore COMMENT
%ignore COMMENT_INLINE

パーサ

from lark import Lark
from lark.exceptions import UnexpectedInput
from lark.visitors import Interpreter


class Parser():
    def __init__(self):
        with open('grammer.lark', 'r') as f:
            grammer = f.read()
        self.parser = Lark(grammer, start='script')

    def parse(self, input_data):
        try:
            tree = self.parser.parse(input_data)
        except UnexpectedInput as e:
            context = e.get_context(input_data)
            print(f'Syntax error:  line = {e.line}  column = {e.column}\n')
            print(context)
            exit(1)

        script = ScriptInterpreter().visit(tree)
        return script


class ScriptInterpreter(Interpreter):
    def script(self, tree):
        return [self.visit(c) for c  in tree.children]

    def statement(self, tree):
        (cmd, args1) = self.visit(tree.children[0])
        args2 = flatten([ self.visit(a) for a in tree.children[1:] ])
        return (cmd, args1 + args2)

    def line(self, tree):
        cmd = self.visit(tree.children[0])
        if len(tree.children) > 1:
            args = flatten(self.visit(tree.children[1]))
        else:
            args = []
        return (cmd, args)

    def continued(self, tree):
        return self.visit(tree.children[0])

    def command(self, tree):
        return tree.children[0]

    def arglist(self, tree):
        args = [ self.visit(a) for a in tree.children ]
        return args

    def arg(self, tree):
        return self.visit(tree.children[0])

    def number(self, tree):
        return float(tree.children[0])

    def string(self, tree):
        return tree.children[0].strip('"')

    def keyword(self, tree):
        return Keyword(str(tree.children[0]))

    def boolean(self, tree):
        return self.visit(tree.children[0])

    def true(self, tree):
        return True

    def false(self, tree):
        return False


class Keyword():
    def __init__(self, val):
        self.val = val

    def __str__(self):
        return f'Keyword<{self.val}>'

    def __repr__(self):
        return f'Keyword<{self.val}>'


def flatten(lis):
    result = []
    for elem in lis:
        if isinstance(elem, list):
            result += flatten(elem)
        else:
            result.append(elem)
    return result

テスト用のスクリプトと実行結果

テスト用なので、入力データ(の内部表現)を組み立てる代わりにコマンドと引数リストを出力する。

from parsers import Parser
import sys


def main():
    parser = Parser()

    with open(sys.argv[1], 'r') as f:
        input_data = f.read()

    script = parser.parse(input_data)

    print('SCRIPT')
    for (cmd, args) in script:
        print('  COMMAND: ' + cmd)
        print('     ARGS: ' + repr(args))



main()

これを実行すると次のようになる。

takatoh@sofa: inputscriptparser-sample > python main.py example.dat
SCRIPT
  COMMAND: TITLE
     ARGS: ['example']
  COMMAND: ADD
     ARGS: [1.0, 2.0]
  COMMAND: MUL
     ARGS: [3.0, 7.0, 8.0, 9.0]
  COMMAND: REPEAT
     ARGS: ['Hello!', 5.0]
  COMMAND: JOIN
     ARGS: ['Hello,', 'world!']
  COMMAND: ADD-2
     ARGS: [5.0, 700.0]
  COMMAND: NO-ARG-COMMAND
     ARGS: []
  COMMAND: COMMAND-WITH-KEYWORD
     ARGS: [Keyword<KW1>, 1.0, Keyword<KW2>, 2.0, Keyword<KW3>, True, Keyword<KW4>, False]

これは期待通り。ところが、何度か実行を続けると、時々次のような結果になる。

takatoh@sofa: inputscriptparser-sample > python main.py example.dat
SCRIPT
  COMMAND: TITLE
     ARGS: ['example']
  COMMAND: ADD
     ARGS: [1.0, 2.0]
  COMMAND: MUL
     ARGS: [3.0, 7.0, 8.0, 9.0]
  COMMAND: REPEAT
     ARGS: ['Hello!', 5.0, Keyword<JOIN>, 'Hello,', 'world!']
  COMMAND: ADD-2
     ARGS: [5.0, 700.0]
  COMMAND: NO-ARG-COMMAND
     ARGS: []
  COMMAND: COMMAND-WITH-KEYWORD
     ARGS: [Keyword<KW1>, 1.0, Keyword<KW2>, 2.0, Keyword<KW3>, True, Keyword<KW4>, False]

コマンドであるべき JOIN が、REPEAT コマンドの引数リストの中に、JOIN というキーワードとして含まれてしまっている(JOIN に続く引数もろとも)。

最初に書いたとおり、文法ファイルも入力ファイルも、パーサも何も変えてない。なのに実行するたびに、JOIN だけコマンドになったりキーワードになったりする。

今のところ全くの原因不明。

気になるところといえば、入力ファイルの JOIN コマンドの前の行が空白文字とコメントであること。さらにもう一つ前の行の行末(5 のうしろ)に空白文字があること(この記事では見えないけど)だ。だけどこれは文法上は問題ないはずに思える。実際期待通りの結果になることもあるんだし。

というわけで、このままでは安心して使えない。どうしようか。