WordPress 5.9

余り使いこなしていないので、イマイチ何が変わったか解らないけど、アナウンスがあったので兎に角アップグレードを行った。サイト改造によりDocker環境での運用のためDockerイメージとしてのwordpressを使用しているが、運用中でもあることなので、普通にダッシュボード画面にてアップグレードで難なく完了。

しかし、ダッシュボードのサイトヘルスステータスを見ると新たなエラーがあった。調べても良く分からないが何やら「intlなるライブラリが足りない」旨のメッセージのようだ。これはwordpress本体の方ではなくサーバー機能の問題ということで、DockerHubへ調査に。wordpress5.9用のfpmイメージがあったのでDockerfileを見てみるとphpのライブラリとしてintlが追加されている。

ということで、Dockerイメージの入れ替え

$ docker-compose down --rmi all

コンテナを停止してイメージ削除、そしてdocker-compose.ymlを編集してをwordpress用phpイメージを5.9に変更

$ docker-compose up -d

にて再起動完了。無事動作することを確認しました。

以前これをやってwordpressのサイトデータがおかしくなった記憶があるので、念のためディレクトリごとバックアップをとっておいたのですが、問題なかったため出番は無し。

mariadbのデータベースも毎日バックアップを取っているので事故があっても面倒なだけで大丈夫な環境にはなっています。

dockerイメージのアップデートは頻繁にあるものもあるので、ときどきはチェックしないといけないと反省。しかもDockerfileやdocker-compose.ymlでのイメージ指定がlatestになっているのが多く、現在のバージョンを調べるのが面倒。しっかりとバージョンを記載したイメージ指定にしないといけないな。。

Windows Terminal

しばらく最新の動向とか触れる機会が少なかったので、最近のツール事情にはいまだに驚かされたりする。当方では趣味でさくらVPSに契約して個人用のサイトを運営をしており、当たり前だが自宅のローカル環境から操作している。今までは通信ソフトのPuttyでサーバーにSSH接続して、サーバー上でemacsなどのエディタで構築作業をおこなっていたが、前回の投稿で書いたようにマイクロソフトのVScodeエディタでSSH接続することでローカル同じ環境で編集作業を行なうことが出来る。

それでも実際ターミナル操作をするときはPuttyを使用していたのだが、今度はWindows Terminalというものを知ることとなる。こちらはWindowsのストアアプリで、コマンドプロンプトからPowershell、WSLも対応しており、プロファイルを追加することでssh接続も可能となる優れもの。

しかもこちらはWindows版のOpenSSHのサービスで動いているので、SSH-Agentサービス(Windowsサービスで自動実行)ログイン時にパスフレーズを入れる必要もないので便利。

ローカル作業時も任意のフォルダで右クリックすると「Windowsターミナルで開く」のメニューが追加されており、Powershellが起動するようになる。(これは、以前よりシフトを押しながら右クリックでPowershellで開くことが可能だったが)

ということで、これも何かあったときのための覚え書き。

VScodeの件もそうだけど、今までは何だったのか?!ってくらい超便利な感じで、きっとまだ触っていないDockerやなんかも同じように感動するんだろうな、、、、と思いつつも、なんだかね。こんなすごい複雑なシステムの開発環境を使って、作っているものと言えば、趣味のオモチャ程度・・・。超豪華な財布の中に2000円くらいしかい入ってない感じw
精進せねばと思うこのごろ。。。

Bootstrap5対応その他

という記事を書こうとWorpressダッシュボードに移動したら、いつのまにかWordpressが5.8にバージョンアップしていた。例によってDBバックアップを取れとの忠告も聞かずに、速攻アップデートボタンポチり。

特に変った様子もないが、変化点は追々チェックするとしよう。

でBootsrtap5対応だが、先月辺りまでのこのサイトの引越作業の時点でベータ版が存在していたが、あまり気にしていなかったところ、ポータルサイト構築を兼ねたWEBアプリ開発の勉強がてら色々調べていたところバージョン5のリリースを知った(今ごろですが)。機能的にはあまり差は無い(というかよく読んでいない)がJQuery依存の削除やIEなど古いブラウザサポート終了など良い感じのアップデートのようなので、まずはdjangoテストサーバー環境で導入実験を行ない、ある程度問題ないことを確認したところで主ブランチに統合して本番環境のポータルサイトでも採用することとした。

見た目はあまり変らないが、テーマのデフォルトの文字サイズなどが異なるため、若干文字が小さかったり、メニューの文字が太字だったりと細かな気にならない違いが見られた。

4.5から5への対応に関する変更点であるが、前回までのSass導入でHTML(テンプレート)にBootstrapのクラスタグを直接書くことは少なくなったので、基本的には階層構造のスタイル定義をしているSassソースファイルを修正することで対応した。

主な違いは、marginやpaddingを表現するクラスで、たとえば右マージンはmr、左マージンはmlというようにrightとleftの頭文字で表現していたが、バージョン5より、左がstart、右がendと表現するようになりそれぞれマージンとしてはms、meと表記するようになった。これはおそらく多言語対応でRTL(右から書く文化)にも対応する事により水平方向の表記は右からと左からというより、始めと終わりという意味で統一したのだろう。

あとハマったのはアコーディオン型のメニューアニメーションの件で、若干名称が異なり、

  • data-toggle → data-bs-toggle
  • data-target → data-bs-target

になっていた。当サイトはこの位しか機能を使っていないので、以上の修正で無事以前と同様に動作するようになった。

というか、せっかくBootstrap5を導入したのに、新機能を調べて(あるのか?)導入実験とかしたいのだが、他にもいろいろやりたいことありで、なかなか優先順位に困っているところ・・・
思いつき順ということもありますが・・・

不正アクセスログ集計ツール④

前回の記事でパイプ接続したプロセスをmapのまま返すとプロセスが終らないまま関数から帰ってきてしまうので、気持ち悪いからlist関数を使ったが、

bipset =  list(map(lambda x: ipv4adrset(x.decode('UTF-8').rstrip('\n')), proc.stdout))
 ↓
bipset =  [ipv4adrset(x.decode('UTF-8').rstrip('\n')) for x in proc.stdout]

の様に、わざわざmap使うより、下のようにリスト内包表記で直接リストを返すような仕組みを用いた方がスマートな気がした。とくに今回のようにmap直後にlistするような使い方の場合特に。

mapやfilterはイテレータなので直後にforなどのループ処理を行なう場合に適しており、リストそのものが欲しいときにはmapを使わずに[リスト内包表記]をするのが良いきがする。リスト内包表記やlist式のリスト生成では各項目の変換式(関数)が一度に使用され結果のリストが返されるが、イテレータでは生成段階では変換式(関数)は実行されず、for文など繰返し処理が行なわれる時に一つずつ実行されることになる。

たとえばファイルを読み込み関数があったとして、オープンしたファイルハンドルに対して読み出して検索や加工をする処理の結果をイテレータとして返してしまうと、関数からリターンした段階でファイルを閉じると、そのイテレータからは読み出せないという結果になる。

ということでその場合は、リストとして返す関数が正解となる。

あと、イテレータからリストに変換するときに一度繰返し処理が行なわれるという考え方からすると、for文に使用する前にリストに変換するのは効率が悪く。動作速度的にも不利になるようなので、そういう観点で使い分けていく目安になるのではないかと思う。

次に、ログファイルの解析部分。これも前回記事で、djangoの認証バックエンドをチョットいじって、ログイン失敗のログを残せるようにしたので、ここではそれを検出してリストとして返すこととする。ログファイルはApacheユーザーに読取り権限があるので、そのまま読見込めばいい。ただし、/var/logに保存している場合など、logrotateの処理が行なわれる場合それも追跡して行ないたい。また古いログはrotate時にgzip圧縮されるのでこれにも対応したい。

from blocklib import ipv4adrset
import os, glob, gzip
logger = getLogger(__name__)
import re, json

# djangoのアクセスログ(json形式)を読み込に認証履歴のレポートを返す
# 認証履歴のログはdjango認証バックエンドを継承して認証時のログ出力機能を追加したもの
# [引数]djlog : 解析するアクセスログ(json形式)をフルパスで与える(gzipもOK)
#              Noneの場合は、デフォルト django_log_json を使用(logorotateされたものも含み全てを読み込む)
#       st : レポート対象の開始日時(logrotateされたファイルはこの日時より新しいものを読み込み対象とする)
#       ed : レポート対象の終了日時
#       address : レポート対象のIPアドレスオブジェクト(Noneの場合は全て)サブネット検索対応
def get_django_loginfails(djlog, st, ed, address=None):
    if djlog is not None:
        djlogs = [djlog]
    else:
        djlogs = [x for x in glob.glob(django_log_json + '*') if re.match('.*(log|gz|[0-9])',x) and os.path.getmtime(x) > st.timestamp()]
        djlogs.sort(key=lambda x: os.path.getmtime(x))
    retlist = []
    for jlog in djlogs:
        logger.debug(jlog)
        with open(jlog, 'r') if not re.match('.*\.gz$', jlog) else gzip.open(jlog, 'rt') as jf:
            #jl = json.load(jf)
            decoder = json.JSONDecoder()
            jlines = map(lambda x: decoder.raw_decode(x), jf)
                #aa = decoder.raw_decode(line)
            for jl in map(lambda x: x[0], jlines):
                # fromisoformatで読み込むために末尾のZが邪魔なので削除
                # UTCのタイムゾーンを設定してJST時刻に変換する
                ts = datetime.fromisoformat(jl['timestamp'].rstrip('Z')).replace(tzinfo=timezone.utc).astimezone(tz_jst)
                ts = ts.replace(microsecond=0)
                if ts > st and ts < ed:
                    (dat, event) = (jl['ip'], jl['event'])
                    res0 = re.match('user logged in : (.+)$', event)
                    res1 = re.match('login faild.*: (.+)$', event)
                    event = False if res1 else True if res0 else None
                    if event is None:
                        continue
                    name = res1[1] if res1 else res0[1] if res0 else ''
                    if address is not None and address != ipv4adrset(dat):
                        continue
                    logger.debug('%s : %s - %s(%s)' % (str(ts), dat, name, event))
                    retlist.append(
                        {
                            'name':name,
                            'datetime':ts,
                            'address':ipv4adrset(dat),
                            'status':event,
                            'source':'django',
                        }
                    )
                    #print('%s : %s : %s' % (str(ts), dat, event))
    return retlist

ログはdjaongo.structlogでjson形式で出力したものを対象としている

  • 17行目globクラスを使用して指定したファイル名で始まるlogrotateファイルのリストを作成する(検索開始日付stよりも新しいタイムスタンプのファイルという条件も含める)
  • それぞれに対して、ファイルオープンし(拡張子gzだったらgzipオープンに変更)1行ずつ読み出し、対象パターンを見つけたら辞書形式のデータの配列として返す

ソースプログラムを貼付けてるけど、実際はそのままでは動かない一部の公開なので誰の役にも立たないただの覚え書きなのであった。
json形式だからさっくり読めるだろうとして始めたら何かエラーになってサッパリ読めない。。何でだろうと思って先人様たちの記事をたどって調べたら、こんなもんだと上記のようにデコーダを作って1行ずつ読み込んでいくようだ。
ぇぇぇーーー。同じくpython使ってjson形式のログを掃き出してるのになんで直接読めないん??ってなった。こんど時間があったら調べてみよう

不正アクセスログ集計ツール②

不正アクセスに対する対応は当サイトでは悪質な(連続で何度もログインアタック攻撃をするなど)場合、手動でblocklist(ufwの場合、/etc/ufw/before.rules)に放り込むことにしている。そのため集計対象としてはブロックリストに含まれるIPアドレスは対象外としたい。よってapacheユーザーでブロックリストを読み込む必要があるが、こちらのファイルはroot権限でしか開くことが出来ない。apacheユーザーを管理者グループに入れることも考えたが、ここはひとつ。

  1. root権限でブロックリストを読み込むコマンドを作成する(getblocklist)。
  2. apacheユーザーに getblocklistコマンドだけパスワードなしでsudo出来るようにする。
  3. apacheユーザー権限で動作するpythonプログラム(djangoなど)でsubproccessにより上記コマンドを実行し、結果をリストとして得る関数を作る

で、バッチリではないかと、この方針で進めることとする。

1.root権限でブロックリストを取得するコマンド

これは全く問題なしで、以前のblockset.pyより該当部分のみを抜き出したプログラムとなる。

getblocklist.py

ipアドレスを利用するクラスipv4addressはライブラリとして分離している。

2.apacheユーザーに実行権限付加

表示プログラムを/usr/local/sbinなどに置きapacheユーザー(www-data)にsudo実行権限を与える

www-data        ALL=(ALL)       NOPASSWD:/usr/local/sbin/getblocklist.py

www-dataパスワード未設定のsu不可ユーザーなのでsudo ALLでも他のコマンドは実行出来ない。

3. apacheユーザー権限でリストを取得する

subproccessモジュールを利用して標準出力を得るには、Popenでプロセスを起動してプロセスの標準出力を接続する必要がある。プロセスのstdoutは一行づつ取り出すイテレータとなるので、mapによりipv4adrsetのリストとして取得している。(また、なぜかプロセスの出力がバイトとなっているので、UTF-8で文字列にデコードする必要があった”x.decode('UTF-8').rstrip('\n')”)

import subprocess

get_blocklist_proccess_cmd = ['sudo','/usr/local/sbin/getblocklist.py']

# root権限でブロックリスト取得コマンドを走らせて標準入力を得る
def get_blocklist():
    proc = subprocess.Popen(get_blocklist_proccess_cmd, stdout=subprocess.PIPE)
    bipset =  list(map(lambda x: ipv4adrset(x.decode('UTF-8').rstrip('\n')), proc.stdout))
    return bipset

良く分かっていなかったのだけど、関数からmapのまま返すとイテレータなので関数の外にでてもプロセスが終わってないのね。。listとして全部取得してプロセスを正常に終らせておく必要がありましたとさ。

旧サーバー停止

今年3月より行なってきたサーバー引越も先月終了。リファレンス用に稼働していた旧サーバー(marocha.marochanet.org)を今月末の契約終了を受けて本日停止しました。これをもって来月より新サーバー(jagha.marochanet.org)単独での運用になります。2013年7月から稼働してきた「さくらVPS(V3)2Gコース」。CentOS6にて運用開始し、2020年初頭にCentOS8にアップグレード。安定してきたところでCentOSサポート終了予定のお知らせ。それを受けてのサーバー引越でした。

さくら「VPS(V5)2GのSSD100Gコース」に無料でSSD100G追加キャンペーンに釣られての流れになるけど、Ubuntuサーバーへの引越と同時に出来て丁度良い感じでした。

まぁ、WEBサイト的には、見た目は何も変らないので変化を感じることは無いけど。。。

新サーバー(jagha):marocha君、8年間お疲れさまでした。
ちなみにmarochaはFinalFnatasyXIをプレイしていたときのメインキャラクター名(ホントはSemicolonというキャラがメインだったのだが、タルタル族の可愛さに徐々のサブキャラのmarochaで活動する機会が増え、最後にはサブがメインになってしまったというオチ)で、jaghaは現在プレイしているFinalFantasyXIVのメインキャラ名です。こちらにもmarochaというサブキャラが居るけど、今のところメインを乗っ取る気配は無さそうだ。。。

Git小技

gitignore

djangoプロジェクトをcommitするときに、コンパイル済みの*.pycなどあとから「このファイルは対象外にしたいなー」てなったとき。

  • .gitignoreに無視したいファイルを追加
  • すでにコミットしているファイルを対象外(リポジトリから削除)とする
.gitignore:
*.pyc

git管理のトップディレクトリに移動して、git rmで対象削除する

find . -name "*.pyc" -exec git rm -f "{}" \;

Tree表示

git log をきれいにみる方法

ここのサイトを参考に

git log --graph --pretty=format:'%x09%C(auto) %h %Cgreen %ar %Creset%x09by"%C(cyan ul)%an%Creset" %x09%C(auto)%s %d'

下記コマンドにてエイリアス登録

git config --global alias.tree 'log --graph --pretty=format:"%x09%C(auto) %h %Cgreen %ar %Creset%x09by"%C(cyan ul)%an%Creset" %x09%C(auto)%s %d"'

各ユーザーごとホームディレクトリの.gitconfigに追加される。

あとエディタも使い慣れたのにしたいので、

$ git config --global core.editor emacs

loggerの件(pythonモジュール)

プログラムのデバッグや各種情報を出力するのにprintを使用したり、エラー出力のためにsys.stderr.writeを使った板が、今後djangoなどWEBアプリ開発を念頭に入れて、loggingモジュールを使用できるよう勉強していきたい。

ここのページを参考にすると、なるほどいつもはモジュールをまんまimportしているが、それだとインスタンスを生成せずに使用してしまう可能性もある。なるべく使用するのみをimportするような習慣をつけておいたほうがよさそう。

×import logging
×logger = logging.getLogger(__name__)

〇from logging import getLogger, StreamHandler, DEBUG, INFO
〇logger = getLogger(?__name__)

あとはおまじないの様に、下記コードを追加して出力先とレベルを設定するとよいそうだ

logger = getLogger(__name__)
handler = StreamHandler()
handler.setLevel(DEBEG)
logger.setLevel(DEBUG)
logger.addHandler(handler)
logger.propagate = False

#
#
logger.info('inform')
logger.error('error occerd')

setLevelは2か所あるので、引数をDEBUGなどを変数に格納して、一括で変えられるようにしてもいいかもしれない。

勉強がてら、上記修正をblockset.pyに適用して、sys.stderr.write()の部分をlogger.info()に変更。動作自体は特に変化はないが、ちゃんと作ってる感じが良いかも・・・
日に日に、情報コピペの覚書投稿になりつつある・・・・

SVGファイルの件

スケーラブルベクターグラフィックス形式といって中身はjavascriptの画像形式で、テキストの入ったような資料を貼付けるのに適していると思う。ブログへの貼付けも通常の画像と同様に貼り付けが可能で、図形拡大による劣化がないのが有難い。今後当サイトでも色々使えそうなので調査してみた。

テキストファイルなのでその気があればテキストエディタで作成・編集が可能だが、基本的にはInkscapeの様な画像ソフトで作成する。Inkscapeの使い方を一からおぼえるのも大変なので、他に手段を考えてみる。Inkscapeでは様々なベクタフォーマットをインポート出来るので、慣れたソフトで作成して変換するのもアリでしょう。

使えそうなフォーマットは*.emf(拡張メタファイル)。これはOffice系からも吐き出せるので便利そう。あとフリーの回路図エディタ水魚堂のBschも*.emfファイルを書き出せるので、回路図を貼付けるの良いかもしれない。ということで早速Bsch付属のサンプル回路図を*.emf出力→InkscapeでSVGに変換してみた。

水魚堂Bschのサンプル回路図

良い感じ。

あと*.wmf(ウィンドウズメタファイル)も対応しているため、簡単な図ならOffice系のDrawで作成して、インポートすれば楽勝じゃん!と思ってPowerPointdeサンプル作って実験してみたところ、インポート時にInkscapeが落ちてしまった。。。

何度やってもダメなため、ネットで調べたが同様の症状は見当たらず、まぁWMF使う人は少ないのかなとでも思っておくことにした。

Officeインポート作戦はあきらめて別の方法を調べていたら、なんと!PowerPointから直接SVGファイルが作成出来るじゃないかー。Office系Drawはある程度使い慣れているので、この方が良いかもしれないが、PowerPointはプレゼンソフトなのでキャンバスサイズがスライドサイズとなり、簡単な図形をそのまま保存すると余白が気になることになる。その場合は保存したい図形を選択した状態でシフトを押しながら右クリックすると「図として保存」という項目があり、そこで出力形式をSVGにすると良い。

作成環境:PowerPoint (Office365) 2021/06/11現在

ちなみに、WordpressでSVGファイルを貼付けられるが、メディアファイルにリンクを設定しても画像ポップアップは出来ないようなので、リンクは無しで貼り付けとする

SVGはソースファイルっぽいので右クリックで保存されないようした方が良さそうだけど、、なんとなく。いや、へんな著作権を主張するんじゃなくて、訪問者の安全性を考えて。。一応javascriptみたいなモンだからねー。

リモートホスト設定・開発環境

パソコンなどのローカル環境から外部サーバー(ここ:さくらVPSなど)に接続して各種設定を行う場合は、基本的にはSSHなどターミナル接続を行い、サーバー上のエディタなどを用いて設定ファイルなどの編集を行っている。

当方の場合は、ローカルのWindowsマシンよりPuttyを使用してSSH接続を行っている。サーバー側もSSHの設定を行いしっかりと鍵ペア認証による接続を行うことで安全性を保っている。ちなみにファイルのコピーには同じ鍵を用いたWinSCPを使用している。

サーバー上での設定ファイルの編集などは、当方ではEmacsを用いている。昔はワークステーション上でも重量級のエディタという印象だったので、チョットしたファイルの編集には不向きということで、専らVi系の軽いエディタを使用していた。まぁ昨今になるとマシンパワーのおかげでそれほど気にするレベルでも無いと言うことで、何となく惰性で使用している。しかし昔ほど気合いの入った設定とかは行う事無くほぼほぼインストールしたままで使用しているのでした。

ただし一点だけ、こだわりではないが、バックアップファイルの自動生成について、デフォルトの設定では、編集してセーブを行うと、元ファイルと同じディレクトリに「(元ファイル名)~」という「~」(チルダ)付きのバックアップファイルが大量発生して何とも見苦しい。その為、バックアップファイルは編集したユーザーのホームの「.saves」ディレクトリにバックアップファイルを世代付きで保存する方式に変更している。

編集対象ファイル例):/home/user-name/project/hogehoge/Readme.md
⇒バックアップファイル:/home/user-name/.saves/!home!user-name!project!hogehoge!Readme.md.~1~

のように元のパス情報を含めたファイル名で世代管理の番号を付けた状態で保存される。下記の設定を

(setq auto-save-default nil)
(setq backup-directory-alist `(("." . "~/.saves")))
(setq backup-by-copying t)
(setq delete-old-versions  t
      kept-ner-versions 6
      kept-old-versions 2
      version-control t)

各ユーザーの設定ファイル(.emacs)に記載しておく、当方の場合は自分以外に使用する人もいないという理由もあって/usr/shara/emacs/下のdefault.elに記載とか乱暴なことをやっている(汗)

続きを読む…