Growiで使っているmongodbのupgrade

普段運用しているgrowiがバージョン4.2.0がリリースされ、mongodb 4.4をサポートする様になったのでアップグレードしてみた。

github.com

アップグレードに関してはドキュメントがあるのでドキュメントの通りにやっていくのが良い。

docs.growi.org

推奨されているdocker-composeで運用している場合、mongodbのバージョンはdocker-compose.ymlでmongodbのdocker imageのバージョンとして指定されている。

  mongo:
    image: mongo:3.6
    restart: unless-stopped
    volumes:
      - mongo_configdb:/data/configdb
      - mongo_db:/data/db
    restart: always

そのためアップグレードする場合は、このmongo:に続く数字を変えた上で、docker-compose stopdocker-compose upをすればいい感じ。簡単ですね。

mongodb公式によると、コンテナが立ち上がったらmongoシェルを起動して多少操作する必要があるらしい。まぁこれも簡単。

docs.mongodb.com

基本的にdocker-compose exec mongo mongodでmongoシェルを起動し、upgradeしたバージョンをdb.adminCommand( { setFeatureCompatibilityVersion: "4.4" } )の様に設定する必要がある。

$docker-compose exec mongo mongod
$db.adminCommand( { setFeatureCompatibilityVersion: "4.4" } )

最初はいけるやろ感があったので、唐突に今動かしている3.6から4.2にupgradeしたところmongodbが立ち上がらなくなった。 調べたところGrowi公式の方も認識していて、mongodbのupgradeはドキュメントに書いてあるとおり段階的にupgradeする必要があるらしい。

dev.growi.org

ということでgrowi公式にある通り

  • MongoDB v3.6 から v4.0 へのアップグレード
  • MongoDB v4.0 から v4.2 へのアップグレード
  • MongoDB v4.2 から v4.4 へのアップグレード

このあたりを順にしてったところ無事upgradeできた。データ消えなくて良かったですね。

singularityを使ったRustコンパイラのデバッグビルド

学科システムにはsingularityというコンテナサービスを導入している。動かしているpodmanと比較するとこのあたりが便利と思っています。

  • ceph fs上でも結構高速に動作
  • root権限がいらない
    • podmanもrootlessではあるが、ceph FS上だと遅い...
  • singularity shellで気軽にコンテナのシェルを立ち上げられる
  • 科学計算向きなのでスパコンなどでの利用実績がある
  • ホームディレクトリなどがattachしたタイミングで自動的にコンテナにマウントされる

とはいえ良い事ばかりではなくツラミポイントもあります。 例えばDockerを使っている場合は構成をDockerfileで書きたくなりますが、singularityで同様のビルドファイルを作ろうとした場合、各コマンドのキャッシュが効きません。

dockerの場合は行単位でキャッシュが効いていたのですが、singularity build的な感じでファイルからビルドしようとすると、失敗した場合最初からになるため、本筋ではないapt updateなどで莫大な時間を浪費します。

しかしsingularityはdockerと違い、--sandboxで実行するといかにもchrootlinux namespaceみがあるディレクトリを作成し、この上で様々な構築をインタラクティブにしたあとにシングルバイナリのsifに固めるという技があるので、dockerファイルの様なファイル形式でなく、インタラクティブに構築する方法が適していそうです。

今回はRustコンパイラデバッグ環境をsingularityで作るのを通して、インタラクティブな構築方法を見てみます。

インタラクティブに構築する場合は、--sandbox--writable を使ってビルド及び環境構築を行います。

sandbox環境の作成

とりあえずaptのupdateだけするファイルrust-debug.defを生成します。

 BootStrap: docker
 From: ubuntu:20.04
 
 %post
     apt-get -y update
     apt-get -y upgrade

sandbox環境のビルド

$singularity build --sandbox --fakeroot rust-debug rust-debug.def

  • sifではなくディレクトリが生成されます
    • ディレクトリの中身はlinux namespaceで区切られていそうな気配ですが、コンテナの中の/移行が置かれています

sandbox環境にシェルログインして環境を作る

$singularity shell --fakeroot --writable rust-debug

こうするとシェルログインが出来ます。 --fakerootはホストOS側でroot権限がないユーザーでも、コンテナの中でrootとして振る舞う事ができるオプションです。

Rustコンパイラデバッグ環境の構築

さてシェルログインできたら、Rustcompilerのデバッグビルドに必要そうなものをインストールします。 なにが必要かはGitHubリポジトリに書いてあるので読んでみます。

github.com

Singularity> apt install -y cmake python3 clang ninja-build lldb gdb curl git

とりあえず必要そうなものをinstall

  • cmake
  • python3
  • clang
  • ninja-build
    • ninjaはaptだとこの名前
  • lldb
  • gdb
  • curl
  • git

無事にインストールされたら、singularityコンテナの中でpythonと入力するとpython3が起動するように仕込みます。

update-alternatives --install /usr/bin/python python /usr/bin/python3 10

参考 Unable to set default python version to python3 in ubuntu

/rootにRustのリポジトリを起きたいところですが、ここは実行ユーザーの$HOMEとマウントされるので適当なディレクトリをつくります

  • $mkdir /rust
  • $cd /rust

Rustコンパイラデバッグビルド

rustのリポジトリをcloneします - $git clone https://github.com/rust-lang/rust.git

Rustはビルドの設定をTOMLで記述します。exampleをもとにするのが良いらしいので、copyします。

  • $cp config.toml.example config.toml

aptでvimをinstallし、設定を書き換えていきます。 まずdebugビルドするので、デバッグオプションを有効化します。 ビルド時にninjaを使いたいのでninjaを有効にし、趣味でinstall先のprefixを変えておきます。

まとめると次のような変更をします。

  • #debug = falsedebug = true
  • #ninja = falseninja = true
  • #prefix = "/usr/local"prefix = "/usr/local"

Rustのビルドとインストール

RustはビルドスクリプトPythonで書かれたx.pyですので、これを実行します

  • ./x.py build && ./x.py install

今回は/rust/rustリポジトリで、ここでビルドを実行しました。 そのため生成されたRustのバイナリは/rust/rust/build/x86_64-unknown-linux-gnu/以下に配置されています。

例えばLLDBでデバッグする場合はrust-lldb /rust/rust/build/x86_64-unknown-linux-gnu/stage1/bin/rustcとするとデバッグ可能です。

ビルドした結果をsifに変換する

sandbox環境で無事ビルドできたので、これを配布可能なsif形式に固めます。

固めるときはbuildで、defファイルでなくディレクトリを指定します。

  • $singularity build --fakeroot debugging-rust-compiler.sif rust-debug

この結果、debugging-rust-compiler.sifが生成されており、以降はこれを使います。

設定したsifに接続する

作成したsifにシェルログインしてみます

$singularity shell debugging-rust-compiler.sif

あわせてよみたい

singularityを使ったRustのデバッグビルド - cr-ryukyu

singularityでRustコンパイラのデバッグ - cr-ryukyu

Perlでgrowiのバックアップファイルからmarkdownを生成する(version1くらい)

研究室ではwikiとしてgrowiを使っています。

growi.cr.ie.u-ryukyu.ac.jp

まぁ便利なのですが、growiはデータベースがmongoDBなので、心もとないという意見や、せっかく階層構造を持っているのでmarkdown単体で保存しておきたいという意見が出ています。

もともとAPI経由でやろうとしていましたが、まぁせっかくなのでbackupファイルからmarkdownを生成してみます。

記事のbackupファイルの取得

growiでは記事はrevisionsというコレクションで登録されています。 growiのwebページからbackupを作成するか、dockerで動かしている場合は次のようなコマンドでrevisonsのjsonを取得します。

$docker exec growi_mongo_1 mongoexport -d growi -c revisions --out revision_back_1013.json
$docker  cp growi_mongo_1:revision_back_1013.json .
$docker exec growi_mongo_1 rm revision_back_1013.json

ここで取得したjsonですが、それぞれの投稿データは次のようなスキーマになっています。

  {
    "_id": "5ecce2e5fc19b9004a86ec47",
    "format": "markdown",
    "createdAt": "2020-05-26T09:35:33.830Z",
    "path": "/user/anatofuz/note/2020/05/26",
    "body": "ここに内容が入る",
    "author": "5df5ef37d744a60045dd1524",
    "hasDiffToPrev": true,
    "__v": 0
  }

注目すべきはcreatedAtはタイムスタンプになっており、authorはgrowiのuserのidとなっています。

authorが具体的に何かはrevisionsだけでは決まりませんので、とりあえず今回は放置します。

pathはgrowiのエントリのタイトルです。これはunixのファイルパスと対応してそうなので、生成したmarkdownはこのパスを利用します。

bodyはエントリの内容ですので、bodyをpathに書かれた場所に書き込む方針を取ります。

markdown自体はgit/hgなどのバージョン管理ツールで管理するのを想定するので、createdAtは最新のものを1つだけ使うようにしてみます。

Perlで軽く書く

というわけでPerlで書いてみました。

みんな大好きPath::Tinyを使っています。

metacpan.org

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

use utf8;
use Encode;
use JSON;
use Path::Tiny;

my $json_file = shift or die 'require json file';
my $revision = decode_json(path($json_file)->slurp);


my $paths;

for my $elem (@$revision) {
   push(@{$paths->{$elem->{path}}}, $elem);
}

for my $path (keys %$paths) {
    my $elems = $paths->{$path};
    print encode_utf8 "$path\n";

    my @sorted_elems   = sort { $b->{createdAt} cmp $a->{createdAt}}  @$elems;
    my $latest_elem    = shift @sorted_elems;

    my $emit_file_path = path("./emit/$path.md");
    $emit_file_path->touchpath;
    $emit_file_path->spew_utf8($latest_elem->{body});
}

__END__
  {
    "_id": "5ecce2e5fc19b9004a86ec47",
    "format": "markdown",
    "createdAt": "2020-05-26T09:35:33.830Z",
    "path": "/user/anatofuz/note/2020/05/26",
    "body": "",
    "author": "5df5ef37d744a60045dd1524",
    "hasDiffToPrev": true,
    "__v": 0
  }

$perl parse.pl revisons.json

の様に使います。

今回は/user/anatofuz/note/2020/05/26の場合は./emit/user/anatofuz/note/2020/05/26.mdというファイルが生成されるようにしてみました。

実際に研究室のGrowiのbackupをもとに実行すると

$ ls emit
611/         Agda.md      Christie/    FileSystem/  Haskell/     Linux/       Sandbox/     software/    user/
611.md       CbC/         Christie.md  Gears/       Haskell.md   Raku/        Sandbox.md   software.md  user.md
Agda/        CbC.md       Events/      Gears.md     Linda.md     Raku.md      growi.md     trash/

の様になっており、例えばRaku.mdを見ると

# Raku

ちょっと前までPerl6と呼ばれていたプログラミング言語

# Docs

- 公式Document
    - https://docs.raku.org/
        - `docs.perl6.org`のものは古いので見ない

# contents

$lsx(/Raku)% 

と無事取れていますね!!!

TODO

まぁ色々雑なのでTODOが多々あります

  • 作者の情報を取得する
  • Hugo等の静的サイトジェネレーターとの組み合わせを考えて、メタ情報をつける
  • 生成パスがこれで良いのか問題
  • できればワンバイナリかfatpackしたい

ということでversion1くらいの話です。まぁこういうの書くの楽しいですよね

pukiwikiのattachファイルをutf8なファイル名に変換する

pukiwikiではファイルアップロードをattachプラグイン経由で実行可能になっている。

attachでアップロードしたファイルは、index.phpがあるディレクトリの、attachディレクトリ以下に次のようなファイルとして保存される。

A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730305F536F6C7574696F6E2E706466*
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730305F536F6C7574696F6E2E706466.log*
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730315F536F6C7574696F6E2E706466*
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730315F536F6C7574696F6E2E706466.log*

これらは何かというと、pukiwikiが使う文字コードをさらにURIエンコードしたものになつている。 これだとパット見でpdfなのかpptなのかjpgなのか判断できないため、utf8ベースのファイル名に変換したい。

今回はeuc-jpの文字コードが使われていたので、decodeしてutf8に変換するやつを書いた

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode encode_utf8/;

while (my $file = <*>) {
  chomp $file;
  $file =~ s/\*//;
  my $ufile = $file;
  $ufile =~s/([0-9A-F]{2})/chr(hex($1))/ge;
  $ufile = decode('euc-jp', $ufile);
  $ufile = encode_utf8 $ufile;
  print "mv $file $ufile\n";
  system("mv", $file, $ufile);
}

このperlをattachディレクトリに置いて実行するといい感じにしてくれる。

mv A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730305F536F6C7574696F6E2E706466 デジタル信号処理/中間テスト_DSPMid'00_Solution.pdf
mv A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730305F536F6C7574696F6E2E706466.log デジタル信号処理/中間テスト_DSPMid'00_Solution.pdf.log
mv A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730315F536F6C7574696F6E2E706466 デジタル信号処理/中間テスト_DSPMid'01_Solution.pdf
mv A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730315F536F6C7574696F6E2E706466.log デジタル信号処理/中間テスト_DSPMid'01_Solution.pdf.log

こんな感じのコマンドが実行されたことになる。いいですね。

なおこれだと自分のプログラムも置換しにいくので、実際は__DATA__以下に変換したいファイル名を書いて実行した。

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/decode encode_utf8/;

while (my $file = <DATA>) {
  chomp $file;
  $file =~ s/\*//;
  my $ufile = $file;
  $ufile =~s/([0-9A-F]{2})/chr(hex($1))/ge;
  $ufile = decode('euc-jp', $ufile);
  $ufile = encode_utf8 $ufile;
  print "mv $file $ufile \n";
}

__DATA__
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730305F536F6C7574696F6E2E706466*
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730305F536F6C7574696F6E2E706466.log*
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730315F536F6C7574696F6E2E706466*
A5C7A5B8A5BFA5EBBFAEB9E6BDE8CDFD2FC3E6B4D6A5C6A5B9A5C8_4453504D69642730315F536F6C7574696F6E2E706466.log*

RustのXML操作ライブラリのquick_xmlを使ってみた

なんとなくRustを勉強している中で、virshのxmlを編集する必要が出てきたため、ググって一番最初に見つかったquick_xmlを使ってみました。

github.com

quick_xmlは特徴としてRustのXMLライブラリの中でも特に早く動くらしいです。

cargo.tomlへの設定

0.20.0が最新っぽいのでこれを使います

[dependencies]
quick-xml = "0.20.0"

実際に使ってみる

SYNOPSISを参考にtemplate.xmlからdump.xmlを作成する例題を書いてみました。

github.com

Readerはquick_xml専用の実装を使う必要があり、Reader::from_readerに任意のReaderを渡すことで作成可能です。

let mut reader = Reader::from_reader(BufReader::new(File::open(file)?));

Writerも同様です。

 let mut writer = Writer::new(BufWriter::new(File::create("dump.xml").unwrap()));

用意したReaderを使って実際にxmlを読んでみます。

公式の通りreader.read_eventで読み進めて、読んだ先の構造を型でマッチングする世界観のようです。

loop {
        match reader.read_event(&mut buf) {
            Ok(Event::Start(ref e)) if e.name() == XML_NAME_ATTRIBUTE => {
                writer
                    .write_event(Event::Start(e.clone()))
                    .expect("faild write event");
                reader.read_event(&mut Vec::new()).expect("faild read event");
                let elem = BytesText::from_plain_str("anatofuz-vm");
                writer.write_event(Event::Text(elem)).unwrap();
            }

            Ok(Event::Text(ref e)) if e.escaped() == b"ie-virsh-template" => {
                let elem = BytesText::from_plain_str("anatofuz-vm");
                writer.write_event(Event::Text(elem)).unwrap();
            }
            Ok(Event::End(ref e)) if e.name() == b"this_tag" => {
                assert!(writer
                    .write_event(Event::End(BytesEnd::borrowed(b"my_elem")))
                    .is_ok());
            }
            Ok(Event::Eof) => break,
            // you can use either `e` or `&e` if you don't want to move the event
            Ok(e) => writer.write_event(&e).unwrap(),
            Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
        }
        buf.clear();
    }

この例ではEvent::Eofが読まれるまで無限ループでxmlをパースし続けます。

Eventは名前からなんとなくわかりますが、例えば<hoge>foo</hoge>のような構造の場合は、<hoge>Event::StartfooEvent::Text、最後の</hoge>Event::Endに対応します。

面白いのは後置のifのようなsyntaxで、特定のEventでかつ、特定の文字列が来た場合などの判定が可能なものです。

例えばこのブロックでは</this_tag>のケースのみ実行されます。

            Ok(Event::End(ref e)) if e.name() == b"this_tag" => {
                assert!(writer
                    .write_event(Event::End(BytesEnd::borrowed(b"my_elem")))
                    .is_ok());
            }

ここで<name>template</name>のようなXMLの構造が来た場合に、中のtemplateを書き換える処理を考えてみます。

まず<name>で来た場合にキャプチャをする必要があるので、Event::Startでキャプチャを行います。

const XML_NAME_ATTRIBUTE: &[u8; 4] = b"name";

///省略

Ok(Event::Start(ref e)) if e.name() == XML_NAME_ATTRIBUTE => {

writerの先のxmlへはwriter.write_eventなど、型によってwriter_*の専用メソッドが用意されています。

この例ではEvent単位で読みながらファイルを書き出していくので、基本的にはwriter.write_eventを使用することになります。

writerへの書き込みはref eで受けていた場合はe.clone()する必要があり、refで受けていない場合はポインタを取るのみで可能です。

writer
        .write_event(Event::Start(e.clone()))
        .expect("faild write event");

次に<name>template</name>templateを書き換える為に、read_eventで更に1EVENT読み進めます。

    reader.read_event(&mut Vec::new()).expect("faild read event");

読んだ先は使わないので特に束縛せず、雑にVec::new()しています。

続いて一度BytesTextインスタンスを作ってから、Event::Text型を作り、これをwrite_event経由で書き込みます。 文字列リテラルからのBytesTextの作成はfrom_plain_strを使うと可能です。

docs.rs

    let elem = BytesText::from_plain_str("anatofuz-vm");
    writer.write_event(Event::Text(elem)).unwrap();

そもそも、xmlのcontentを決め打ちでリプレイスする場合は、Event::Textでマッチングすると楽です。

Ok(Event::Text(ref e)) if e.escaped() == b"ie-virsh-template" => {
    let mut elem = BytesText::from_plain_str("anatofuz-vm");
    writer.write_event(Event::Text(elem));
}

調子に乗ってentrypoint.shのshebangを省略しない

元気に$podman runしたところ、下記のようなエラーが出た

standard_init_linux.go:211: exec user process caused "exec format error"

一体何を...バイナリでも壊したかな.......みたいな気分だったが、Dockerfileで最後のCMDとして指定している

CMD ["./entrypoint.sh"]

entrypoint.shの中身が

cd exapmle
python main.py

みたいなシェルスクリプトで、 戦闘に#!/bin/shが無いという落ちだった。。。 ぐぐると様々な人間が引っかかっている。普通のスクリプト言語でプログラミングするときは省略しないはずなのに、やはりdockerスクリプトを書くときみたいな勢いでエイヤしてるときは引っかかりがちなので、気をつけていきたい...

www.lewuathe.com

Rustで文字列が数値かどうかの判定

String変数を.parseしてOkかErrかどうかで判定可能。 たぶん<u8>じゃなくて<u16>とかにすると範囲が拡大する。

    let name = match name.parse::<u8>() {
        Ok(_) => String::from(format!("{}-{}", user_name, name)),
        Err(_) => name,
    };

この例だと数値が来ていた場合はuser_nameとハイフンで結んだ文字列をnameにshadowingしている。anatofuz-01的な感じ。