株式会社クリアコード > ククログ

ククログ


Groongaのウインドウ関数を使って特定範囲の値を集計する

データベースなどでまとまったデータを取り扱っていると、ある特定の範囲の値を集計したいことがあります。

例えば、全国展開している商店で、全店舗の売上を合計して一ヶ月の売上を算出するケースを考えてみましょう。
この場合は、PoatgreSQL等のRDBMSではGROUP BY句を、Groongaではドリルダウンを使えば集計できます。
ですが、例えば、各店舗が全体の売上に対してどの程度寄与しているかを知りたい場合、RDBMSのGROUP BY句やGroongaのドリルダウンでは集計できません。

こういったケースでは、 "各店舗の売上/全体の売上" を計算する必要があります。

具体例として、各店舗の月の売上を集計したテーブルを考えます。このテーブルを使って、各店舗の売上が全体の売上に対してどの程度寄与しているかを算出します。

この例では、各店舗の売上は既にカラムとして用意されているので、その値を使えば良いですが、全体の売上は集計する必要があります。
この時、RDBMSのGROUP BY句や、Groongaのドリルダウンでは、集計結果のみを結果として返すので、集計結果と既存の他のカラムの値を使って計算するということができません。

こういった時に使えるのがウインドウ関数と呼ばれる関数群です。
ウインドウ関数も、RDBMSのGROUP BY句や、Groongaのドリルダウンと同様、ある特定範囲の値を集計します。
異なるのは、結果を全てのレコードに追加する点です。

つまり、ウインドウ関数を使うと各レコードに全体の売上を格納するカラムが追加されます。
全体の売上がカラムとして追加されるので、レコード毎に既存のカラムと組み合わせて "各店舗の売上/全体の売上" も計算できます。

では、実際に各店舗の売上が全体の売上に対してどのくらいの割合なのかをGroongaで計算してみましょう。

まずは、各店舗の月の売上を集計したテーブルを用意します。

table_create ReportPerMonth TABLE_NO_KEY
column_create ReportPerMonth branch_name COLUMN_SCALAR ShortText
column_create ReportPerMonth sales COLUMN_SCALAR UInt32
column_create ReportPerMonth month COLUMN_SCALAR Time

load --table ReportPerMonth
[
{"branch_name": "Sayama"   , "sales": 256125  , "month": "2020-04-30 00:00:00.000000"},
{"branch_name": "Iruma"    , "sales": 211546  , "month": "2020-04-30 00:00:00.000000"},
{"branch_name": "Hakodate" , "sales": 122928  , "month": "2020-04-30 00:00:00.000000"},
{"branch_name": "Otaru"    , "sales": 1132001 , "month": "2020-04-30 00:00:00.000000"},
{"branch_name": "Umeda"    , "sales": 188122  , "month": "2020-04-30 00:00:00.000000"},
{"branch_name": "Kure"     , "sales": 120160  , "month": "2020-04-30 00:00:00.000000"},

{"branch_name": "Sayama"   , "sales": 156125  , "month": "2020-05-30 00:00:00.000000"},
{"branch_name": "Iruma"    , "sales": 71546   , "month": "2020-05-30 00:00:00.000000"},
{"branch_name": "Hakodate" , "sales": 111928  , "month": "2020-05-30 00:00:00.000000"},
{"branch_name": "Otaru"    , "sales": 1332001 , "month": "2020-05-30 00:00:00.000000"},
{"branch_name": "Umeda"    , "sales": 117122  , "month": "2020-05-30 00:00:00.000000"},
{"branch_name": "Kure"     , "sales": 1120160 , "month": "2020-05-30 00:00:00.000000"},

{"branch_name": "Sayama"   , "sales": 1561250 , "month": "2020-06-30 00:00:00.000000"},
{"branch_name": "Iruma"    , "sales": 211546  , "month": "2020-06-30 00:00:00.000000"},
{"branch_name": "Hakodate" , "sales": 199928  , "month": "2020-06-30 00:00:00.000000"},
{"branch_name": "Otaru"    , "sales": 1452001 , "month": "2020-06-30 00:00:00.000000"},
{"branch_name": "Umeda"    , "sales": 111122  , "month": "2020-06-30 00:00:00.000000"},
{"branch_name": "Kure"     , "sales": 100160  , "month": "2020-06-30 00:00:00.000000"},
]

branch_name が各店舗の名前です。 sales はその月の売上です。 month は売上月です。

データをロードしたら次にウインドウ関数を使って、月毎の全体の売上を集計しましょう。
まずは、 --columns[LABEL].window.group_keysmonth を指定して月毎にグループ化します。

全体の売上は、月毎の売上の総和なので、 --columns[LABEL].valuewindow_sum(sales)
使って計算します。
window_sumwindow.group_keys でグループ化したグループ毎に指定したカラムの値の総和をとります。

つまり以下のようなクエリーを実行します。

plugin_register functions/time

select ReportPerMonth \
  --limit -1 \
  --columns[sales_sum_per_month].stage initial \
  --columns[sales_sum_per_month].type UInt32 \
  --columns[sales_sum_per_month].value 'window_sum(sales)' \
  --columns[sales_sum_per_month].window.group_keys month \
  --output_columns '_id, branch_name, sales, sales_sum_per_month, time_format_iso8601(month)'

上記のクエリーの結果は以下の通りです。
ポイントは、月毎の全体の売上を表す sales_sum_per_month が全てのレコードに追加されているところです。
月毎に同じ値がsales_sum_per_monthに設定されていることが確認できるかと思います。

例えば、2020年4月の全体の売上は、2030882となっているので、売上月が2020-04-01T00:00:00.000000+09:00
のレコードのsales_sum_per_monthは、全て2030882が設定されています。

[
  [
    0,
    1594109666.415102,
    0.002141475677490234
  ],
  [
    [
      [
        18
      ],
      [
        [
          "_id",
          "UInt32"
        ],
        [
          "branch_name",
          "ShortText"
        ],
        [
          "sales",
          "UInt32"
        ],
        [
          "sales_sum_per_month",
          "UInt32"
        ],
        [
          "time_format_iso8601",
          null
        ]
      ],
      [
        1,
        "Sayama",
        256125,
        2030882,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        2,
        "Iruma",
        211546,
        2030882,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        3,
        "Hakodate",
        122928,
        2030882,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        4,
        "Otaru",
        1132001,
        2030882,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        5,
        "Umeda",
        188122,
        2030882,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        6,
        "Kure",
        120160,
        2030882,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        7,
        "Sayama",
        156125,
        2908882,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        8,
        "Iruma",
        71546,
        2908882,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        9,
        "Hakodate",
        111928,
        2908882,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        10,
        "Otaru",
        1332001,
        2908882,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        11,
        "Umeda",
        117122,
        2908882,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        12,
        "Kure",
        1120160,
        2908882,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        13,
        "Sayama",
        1561250,
        3636007,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        14,
        "Iruma",
        211546,
        3636007,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        15,
        "Hakodate",
        199928,
        3636007,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        16,
        "Otaru",
        1452001,
        3636007,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        17,
        "Umeda",
        111122,
        3636007,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        18,
        "Kure",
        100160,
        3636007,
        "2020-06-01T00:00:00.000000+09:00"
      ]
    ]
  ]
]

これで、月毎の全体の売上が集計できました。
次は、各店舗の売上が全体の売上の何%なのかを算出します。

月毎の全体の売上は、前述の通り sales_sum_per_month カラムに格納されています。
このカラムは全レコードに追加されているので、このカラムの値と、各店舗の売上 salessales / sales_sum_per_month のように除算することで、各店舗の売上が全体の売上の何%なのかを算出できます。

結果は、each_sales_per_allカラムに格納されます。

sales / sales_sum_per_monthの値は実数になる可能性があるので、実際のクエリーでは、sales * 1.0 / sales_sum_per_monthのようにsalesの値に* 1.0して、計算結果の型を整数から実数にしています。

select ReportPerMonth \
  --limit -1 \
  --columns[sales_month].stage initial \
  --columns[sales_month].type Time \
  --columns[sales_month].value 'time_classify_month(month)' \
  --columns[sales_sum_per_month].stage initial \
  --columns[sales_sum_per_month].type UInt32 \
  --columns[sales_sum_per_month].value 'window_sum(sales)' \
  --columns[sales_sum_per_month].window.group_keys sales_month \
  --columns[each_sales_per_all].stage output \
  --columns[each_sales_per_all].type Float \
  --columns[each_sales_per_all].value 'sales * 1.0 / sales_sum_per_month' \
  --output_columns '_id, branch_name, sales, sales_sum_per_month, each_sales_per_all, time_format_iso8601(sales_month)'
[
  [
    0,
    1594109970.315803,
    0.003779888153076172
  ],
  [
    [
      [
        18
      ],
      [
        [
          "_id",
          "UInt32"
        ],
        [
          "branch_name",
          "ShortText"
        ],
        [
          "sales",
          "UInt32"
        ],
        [
          "sales_sum_per_month",
          "UInt32"
        ],
        [
          "each_sales_per_all",
          "Float"
        ],
        [
          "time_format_iso8601",
          null
        ]
      ],
      [
        1,
        "Sayama",
        256125,
        2030882,
        0.1261151558780865,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        2,
        "Iruma",
        211546,
        2030882,
        0.1041645944963814,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        3,
        "Hakodate",
        122928,
        2030882,
        0.0605293660586878,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        4,
        "Otaru",
        1132001,
        2030882,
        0.5573937826028297,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        5,
        "Umeda",
        188122,
        2030882,
        0.09263068952307421,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        6,
        "Kure",
        120160,
        2030882,
        0.05916641144094044,
        "2020-04-01T00:00:00.000000+09:00"
      ],
      [
        7,
        "Sayama",
        156125,
        2908882,
        0.05367182305779334,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        8,
        "Iruma",
        71546,
        2908882,
        0.02459570377897763,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        9,
        "Hakodate",
        111928,
        2908882,
        0.03847801320232309,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        10,
        "Otaru",
        1332001,
        2908882,
        0.4579082272845719,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        11,
        "Umeda",
        117122,
        2908882,
        0.04026357892826179,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        12,
        "Kure",
        1120160,
        2908882,
        0.3850826537480723,
        "2020-05-01T00:00:00.000000+09:00"
      ],
      [
        13,
        "Sayama",
        1561250,
        3636007,
        0.4293858620184174,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        14,
        "Iruma",
        211546,
        3636007,
        0.05818085608746078,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        15,
        "Hakodate",
        199928,
        3636007,
        0.05498559271200523,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        16,
        "Otaru",
        1452001,
        3636007,
        0.3993394402155991,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        17,
        "Umeda",
        111122,
        3636007,
        0.03056154732375378,
        "2020-06-01T00:00:00.000000+09:00"
      ],
      [
        18,
        "Kure",
        100160,
        3636007,
        0.02754670164276361,
        "2020-06-01T00:00:00.000000+09:00"
      ]
    ]
  ]
]

このようにして、各店舗の売上が全体の売上のどのくらいなのかを計算できました。

ウインドウ関数を使って、ある特定範囲の値を算出し、その値を既存のカラムの値と組み合わせて計算する例を紹介しました。
この例のように、集計した値と、既存のカラムの値を組み合わせてなにか計算したい場合にウインドウ関数の使用を検討してみてはいかがでしょうか?

タグ: Groonga
2020-08-03

ノータブルコード11 - 空になるかもしれないCのマクロの値を正規化

最近、GNU/Linux上のGCCとVisual C++のビルドでは同じ挙動なのにMinGWのビルドでだけ挙動が異なる件を調べていた須藤です。MinGWが提供するヘッダーファイルを見ていたら「お!」と思うコードがあったので11回目のノータブルコードとして紹介します。

MinGWはGCCでVisual C++でビルドしたようなバイナリー(Windowsが提供するランタイムで動くバイナリー)を出力するためにいろいろ頑張っています。標準入出力まわりもその1つです。ただ、Windowsが提供する標準入出力機能はANSI Cで定義されている標準入力機能と仕様が異なることがあります。たとえば、snprintf()の第二引数の挙動が違います。Windowsでは第二引数は書き込める最大文字数(終端の\0を含まない)ですが、ANSI Cでは終端の\0を含んだバイト数です。(ANSI Cの仕様はこれであってる?)

たとえば、次のプログラムで違いが出ます。

#include <stdio.h>

int
main(void)
{
  char buffer[5];
  snprintf(buffer, 5, "hello");
  printf("%.*s\n", 5, buffer);
  return 0;
}

Windows上では次の結果になります。

hello

Debian GNU/Linux上では次の結果になります。

hell

この例は実は今回の話とはまったく関係ない(!)のですが、MinGWには__USE_MINGW_ANSI_STDIOというマクロで標準入出力機能の実装を切り替えることができます。__USE_MINGW_ANSI_STDIOというマクロをユーザーが指定することもあるので正規化していました。次のコードです。

/* We are defining __USE_MINGW_ANSI_STDIO as 0 or 1 */
#if !defined(__USE_MINGW_ANSI_STDIO)
#define __USE_MINGW_ANSI_STDIO 0      /* was not defined so it should be 0 */
#elif (__USE_MINGW_ANSI_STDIO + 0) != 0 || (1 - __USE_MINGW_ANSI_STDIO - 1) == 2
#define __USE_MINGW_ANSI_STDIO 1      /* was defined as nonzero or empty so it should be 1 */
#else
#define __USE_MINGW_ANSI_STDIO 0      /* was defined as (int)zero and non-empty so it should be 0 */
#endif

ここで私が「お!」と思ったのは次の部分です。

#elif (__USE_MINGW_ANSI_STDIO + 0) != 0 || (1 - __USE_MINGW_ANSI_STDIO - 1) == 2

これは、__USE_MINGW_ANSI_STDIOとして0以外の値(空の値も含む)を指定されたら真になります。(数値以外の値が指定されていたらエラーです。)

たとえば、0が指定されていれば次のようになって偽になります。

#elif (0 + 0) != 0 || (1 - 0 - 1) == 2

#elif 0 != 0 || 0 == 2

たとえば、1が指定されていれば次のようになって真になります。

#elif (1 + 0) != 0 || (1 - 1 - 1) == 2

#elif 1 != 0 || -1 == 2

たとえば、2が指定されていれば次のようになって真になります。

#elif (2 + 0) != 0 || (1 - 2 - 1) == 2

#elif 2 != 0 || -2 == 2

たとえば、空の値が指定されていれば次のようになって真になります。これがおもしろかったんです!

#elif (+ 0) != 0 || (1 - - 1) == 2

#elif 0 != 0 || (1 - (-1)) == 2

#elif 0 != 0 || 2 == 2

二項演算子に見えた式が単行演算子になって全体として妥当な式になります。Cのマクロはあまり柔軟性がありませんが、こんな風に書くと01に値を正規化できるんだなぁと感心しました。コメントがないとすぐにはピンとこないコードだと思うのでそんなに使う機会はない気がしますが。。。

今回はMinGWのヘッダーファイルにあるコードで「お!」と思った正規化方法を紹介しました。

ところで、そろそろみなさんも自分が「お!」と思ったコードを「ノータブルコード」として紹介してみたくなってきませんか?ということで、このブログへの寄稿も受け付けることにしました。まだ仕組みは整えていないのですが、とりあえず、 https://gitlab.com/clear-code/blog/issues にMarkdownっぽいマークアップで書いた原稿を投稿してもらえばいい感じに調整してここに載せます。寄稿したいという人がたくさんいるならもう少しちゃんとした仕組みを整えようと思っていますので、興味のある人はご連絡ください。寄稿してもらった記事の著作者は作者本人のままですが、ライセンスはCC BY-SA 4.0GFDL(バージョンなし、変更不可部分なし、表表紙テキストなし、裏表紙テキストなし)のデュアルライセンスにしてください。参考:ククログのライセンス

それでは、次のノータブルコードをお楽しみに!

2020-08-06

Thunderbird 78に未対応のアドオンのThunderbird 78対応の現状と課題について

Mozillaサポート業務に従事している結城です。

去る7月17日、Thunderbird 78がリリースされました。この記事では、Flex Confirm MailのThunderbird 78対応版開発作業*1を通じて得られた知見を元に、「アドオンのThunderbird 78対応化」が可能かどうかの大まかな判断基準や、参考となる情報をご紹介します。

Thunderbird 78における従来型アドオン対応終了の経緯

Thunderbird 78では従来形式のアドオン(以下、「XULアドオン」と表記します)が一切動作しなくなり、WebExtensionsと呼ばれるAPIセット*2に基づく新形式のアドオン(以下、「WebExtensionsアドオン)のみが動作するようになりました。これは、Firefox 57において行われた、XULアドオンの廃止によるWebExtensionsアドオンへの一元化に相当する変化となります。

Thunderbirdは、対応するバージョン番号のFirefoxがベースになっているため本来であれば、XULアドオンはThunderbird 60で動作しなくなるところでした。しかし、ThunderbirdのWebExtensions APIは開発リソースの不足のために機能が揃っておらず、Thunderbird 60やThunderbird 68の時点でXULアドオンに対応しなくなると、代替手段がなくなってしまう状況でした。そのため、Firefoxでは無効化された機能を復活させたり、廃止された機能を代替する独自の互換レイヤーを実装したりして、XULアドオンの延命措置が図られていました。

今回のThunderbird 78では、WebExtensions APIの実装がある程度の完成度に達したため、XULアドオンの延命に終止符が打たれ、ようやく満を持してWebExtensionsアドオンへの全面移行が行われたということになります。

XULアドオンとWebExtensionsアドオンの相違点

WebExtensionsアドオンは、エンドユーザーからはXULアドオンと区別が付かなくても、開発者側視点ではまったくの別物です。
(FirefoxにおけるXULアドオンとWebExtensionsアドオンの差異を示した図。XULアドオンはFirefoxの内部にUIからバックエンドまで深く癒着しているのに対し、WebExtensionsアドオンはサンドボックス内に公開されたAPIのみに依存するため、疎結合となっている。)
こちらはFirefoxにおけるXULアドオンとWebExtensionsアドオンの差異を図示したものですが、この説明はThunderbirdにもそのままあてはまります。

XULアドオンは、実態としてはThunderbirdに対して動的に適用されるパッチに近い存在でした。そのため、Thunderbirdの動作を自由自在に書き換えることができ、非常に自由度が高いのが特徴でした。その反面、パッチを当てる対象となるThunderbirdの内部実装に対して極めて密結合となるため、Thunderbird側の変更の影響を受けやすく、Thunderbirdの更新の度にどこかしらの部分が動作しなくなりやすい性質がありました。

これに対しWebExtensionsアドオンは、サンドボックス化された環境の中で動作し、WebExtensions APIとして用意されたインターフェースのみを通じてThunderbirdと相互に通信する、独立した小型のソフトウェアと言えます。Web開発者の視点では実質的には、Thunderbirdというバックエンドと通信するためのWebExtensions APIというアダプターを使った、小規模なWebアプリ(のフロントエンド) とも言えるでしょう。Thunderbirdの内部実装に対しては疎結合となるため、Thunderbirdの更新で動かなくなるリスクは大幅に減じられました。その一方で、APIが用意されていないことは一切行えず、自由度の幅は大きく制約されています。

このような違いがあるため、XULアドオンからWebExtensionsアドオンへの更新作業(WebExtensions化)は、特にXULアドオンとして適切に設計されていた物ほど、「Thunderbird 78に対応するための小規模な改修」と言うよりも、「既存のアドオンの動作をリファレンスとして、それと同等の結果を得られるようなソフトウェアを新たに開発する」と言った方が実態に近い、極めて大がかりな作業となります*3

そのため、いくつかの著名アドオンについて、WebExtensions化を支援するクラウドファンディングが行われた事例もあります。このクラウドファンディングは目標金額を達成したようですが、本稿執筆時点で各アドオンのWebExtensions版は未リリースであることからも、上記の作業の大変さを窺い知れます。

WebExtensionsアドオン開発の注意点

「アドオンのWebExtensions化」の成功に必要なポイントは、

  • したいことを実現するのに必要な機能がWebExtensions APIで提供されているかどうか、WebExtensionsの仕組み上で許可されているか
  • WebExtensions APIが用意されていない部分をどのように実装するか

の2点に集約されます。

WebExtensions APIの傾向

FirefoxのWebExtensions APIの情報はMDNに集約されていますが、ThunderbirdのWebExtensions APIの情報は、OSS向けの技術文書ホスティングサイトであるRead The DocsにThunderbird WebExtension APIsとして集約されています。実際のアドオン開発にあたっては、導入部はMDNのチュートリアルを参照しつつ適宜Thunderbird向けに読み替えて、要領が掴めてきたらThunderbirdに固有のAPIをThunderbird用のドキュメントで調べる、という要領になるでしょう。

全体的な傾向としては、以下の事が言えます。

  • Thunderbirdの各ウィンドウの内容には、ブラウザの「タブ」としてアクセスする。
    • メール表示ウィンドウと編集ウィンドウにはタブは存在しないが、「タブが1つだけ開かれているウィンドウ」として扱われる。
  • ツールバーボタンは1アドオンにつき、メール表示画面用に最大1つ、メール編集画面用に最大1つまで登録できる。
  • 表示中・編集中のメールの本文は「コンテンツ」として扱われ、WebExtensions APIで言うところのコンテンツスクリプトで操作する。
  • アドレス帳やメールフォルダなどへのアクセス方法は、WebExtensionsでのブックマークの操作に似ている。
  • Thunderbird上で発生したイベント・行われた操作は、原則として事後的に通知されるため、アドオンからのキャンセルはできない。

APIの一部はFirefoxと共通で、詳細はMDNを参照するように誘導されている場合があります。ただ、Firefoxでは行えるはずのことがThunderbirdの同名のAPIでは行えない部分もいくつかあります。筆者自身、Thunderbird用のWebExtensionsアドオンの開発中にいくつかそのようなトラブルに遭遇し、以下の通りバグとして報告しました。

このように、細かい部分ではまだまだ不具合が散見されるため、APIを使う際には若干の注意と工夫が必要です。

なお、Thunderbirdの内部的な実装にアクセスする実験用APIも存在していますが、このAPIの使用はおすすめできません。今後のThunderbirdの開発ロードマップにも記されているとおり、今後はThunderbird内の古い実装はどんどん廃止されていく予定のため、このAPIに依存するアドオンでは、「Thunderbird本体が更新されたらアドオンが動かなくなる」というXULアドオン時代の問題が継続してしまうからです。

APIでカバーされない、ユーザー体験に直接関わる部分

WebExtensions APIは「アドオンを作りやすくする便利機能」ではなく「Thunderbirdをアドオンから操作するための基盤」として設計されています。昨今のWeb APIの仕様と同様、ユーザー体験に直接関わる部分はアプリケーション(アドオン)開発者側で工夫して実現する必要があります。そのような工夫を一般化した物が「ライブラリ」や「ツールキット」です*4

Webページ用としては、昔からjQuery UIなどの独自のUIツールキットがいくつも開発されてきました。WebExtensionsアドオンは実質的には小規模なWebアプリだと述べましたが、このようなWebアプリ用ツールキットをWebExtensionsアドオンでも使うことができます。

ただ、Webページ用のUIツールキットは「Webページの中でダイアログのような物を表示する」のように、良くも悪くもWebページ内で動作が閉じる前提であることが多いです。Webページよりももう少し自由度が高いWebExtensionsのニーズにはマッチしないこともありますし、設定の永続化などWeb APIではなくWebExtensions固有のAPIを使った方が適切な場合も多いです。

このような理由から筆者自身がWebExtensionsアドオン用に開発したライブラリはいくつかあり、このブログでも過去にいくつか使用法をご紹介してきました。

当社でのThunderbird向けWebExtensionsアドオンの開発過程でも、UIとして任意の内容のダイアログウィンドウを開く場面で工夫や回避策が必要であったことから、ダイアログ形式のUIの実装を支援するライブラリを新規に開発しました。

XULアドオンのWebExtensions化に取り組む際の注意点

既存のXULアドオンをWebExtensions化する際には、これらの前提を踏まえた上で、前提にフィットするようにアドオンの仕様を見直すことが必要となります。

すでに述べたとおり、XULアドオンとWebExtensionsアドオンは前提が大きく異なります。「XULアドオンを最小限の変更でWebExtensions化する」「XULアドオンで実現していた機能をすべてWebExtensionsアドオンに持ち込む」という発想のままで作業に取り組むと、「あれもできない、これもできない」と様々な壁に阻まれて、結局WebExtensions化自体を断念する他なくなってしまいます。

そうではなく、まずはアドオンの各機能を「その機能は何のために・どういう目的で用意したのか?」にまで立ち返って分析し、「WebExtensions APIの上でその目的を達成する」という発想で設計し直すことが肝心です。その上で、最終的なユーザー体験をXULアドオンでの物に近付けるよう工夫するのが、XULアドオンのWebExtensions化では有効なアプローチと言えます。

多機能なXULアドオンほど、WebExtensions APIで実現できる機能とそうでない機能が混在している場合は多いです。そのようなケースでは、機能ごとに個別のアドオンとして切り出すのも有効です。「全機能が揃わないのですべて諦める」よりは、「一部の機能だけでもThunderbird 78で使える」方がユーザー体験として望ましいことに、疑いの余地は無いでしょう。

当社開発のアドオンの中で、WebExtensions化は不可能と見込まれる物

当社がThunderbird Add-onsで公開しているThunderbirdアドオンは多数あります*5。この中で、具体的にThunderbird 78対応のための作業が進行しているのは、現時点ではFlex Confirm Mailのみとなります。

それ以外のアドオンについてWebExtensions化が可能かどうかの予備調査を実施した所、以下のアドオンは必要なAPIが存在しないために、WebExtensions化はできない可能性が高いと分かりました。前述の傾向と併せて、「どのようなアドオンであればThunderbrid 78に対応できて、どのようなアドオンは対応できないか」を見積もる際の参考にしていただければ幸いです。

なお、WebExtensions化の可否以前のこととして、以下のアドオンは必要性がなくなったため、今後の更新は行われません。

まとめ

以上、Thunderbird用のXULアドオンをThunderbird 78以降に対応させる上で必要となる情報をまとめてみました。

Firefox 57でのXULアドオン廃止は、Firefoxユーザーに未曾有の大混乱を引き起こしました。現時点ではThunderbirdではまだ深刻な混乱は発生していませんが、Thunderbird 68.12のサポート終了のタイミングでThunderbird 78への自動更新が始まるため、その時には何らかの対応が必要となります*6

当社では、業務上で重要度の高いアドオン(当社製でない物も含む)のThunderbird 78対応作業も含む、Thunderbirdの法人利用者向けの有償サポートを提供しております。全社的に使用中のアドオンがThunderbird 78で動作しないなどのトラブルでお困りの情シス担当者さまがいらっしゃいましたら、お問い合わせフォームよりご連絡下さい。

また当社では、このような「OSSの改善に関わりつつ、それを仕事とすること」に関心のある方のエンジニア採用を行っています。日常的にOSSを使用していて、自分でもOSSに関わること自体を仕事にしてみたいという方は、ぜひお問い合わせいただければ幸いです。

*1 現在も進行中で、作業は未完了です。

*2 ドキュメントによっては「MailExtensions」という表記も見られますが、多くのドキュメントではFirefoxと同じ「WebExtensions」と表記されているため、この記事ではこちらに統一することにします。

*3 XULアドオンで使用されていたコードの一部をWebExtensionsアドオンで流用できる場合はありますが、作業方針としてはあくまで「新規開発」と捉える方が実態に即しています。ただ、XULアドオンらしくない・一般的なWebアプリの作法で作られていたXULアドオンは、流用できるコードの量が多くなるとは言えます。

*4 XULアドオンで言えば「OS標準のダイアログと同様に振る舞うダイアログ」や「文字・数値の入力が可能なドロップダウンリスト」などがツールキットの領域にあたります。

*5 法人サポート契約の中で必要が生じてご依頼を頂き作成した物を一般公開しているケースについては、まだ作業のご依頼を頂いていないため現時点でThunderbird 68に未対応の状態の物もあります。

*6 自動更新を停止して古いバージョンを使い続けるユーザーも少なくないですが、安全性の観点からはお薦めできません。

タグ: Mozilla
2020-08-07

D-Feetを使ってDBusインターフェースの変更に追従するには

はじめに

ソフトウェアをメンテナンスしていると、バージョンアップにともないAPI等が変更になることがあります。
今回は、アプリケーションが対応しているDBusのインターフェースの変更に追従するやりかたを紹介します。

DBusのインターフェースの変更に気づいたきっかけ

問題に気づいたきっかけは、#955899 growl-for-linux: Depends on deprecated dbus-glibというバグ報告でした。

バグ報告で言及されているgrowl-for-linuxは、Growl Notification Transport Protocolを使う通知アプリケーションです。
アプリケーションで使っているライブラリーであるdbus-glibが非推奨になっているというのが報告されていました。

growl-for-linuxはRhythmboxのプラグインがあり、音楽を再生するときにgrowl-for-linux経由で通知できます。
バグを修正するためにdbus-glibからGDBusへ移行する際に、このプラグインが動作しなくなっていることがわかりました。
その原因がDBusのインターフェースの変更だったのです。

D-Feetを使って問題を特定する

D-FeetはDBusのデバッガでシステムバスやセッションバスの情報を表示できます。

動作しなくなっていたプラグインは次の条件を前提としていました。

参考:rhythmbox.c

  • セッションバスのバス名は org.gnome.Rhythmbox
  • Rhythmboxのオブジェクトパスは /org/gnome/Rhythmbox/Shell と /org/gnome/Rhythmbox/Player
  • オブジェクトパス /org/gnome/Rhythmbox/Player の getPlayingUri で再生中の曲のパスを取得する
  • オブジェクトパス /org/gnome/Rhythmbox/Shell の getSongProperties で再生中の曲のプロパティを取得する

一方、D-FeetでRhythmbox(バージョンは3.4.4)をみてみると次のことがわかりました。

d-feetでセッションバスを確認しているスクリーンショット

  • セッションバスのバス名は org.gnome.Rhythmbox3 (Rhythmbox3に変わっている)
  • Rhythmboxのオブジェクトパスは /org/mpris/MediaPlayer2 *1
  • オブジェクトパス /org/mpris/MediaPlayer2 の PlaybackStatus プロパティで再生状態を取得する
  • オブジェクトパス /org/mpris/MediaPlayer2 の Metadata プロパティで曲のメタ情報を取得する *2

したがって、上記のとおりD-Busのインターフェースに対してアクセスしている箇所を移植することで動作するようになりました。

修正内容は Use GDBus instead of deprecated dbus-glib としてupstreamにもフィードバックしています。

まとめ

今回は、アプリケーションが対応しているDBusのインターフェースの変更に追従するやりかたを紹介しました。*3
もし、DBusのインターフェースの変更に対応することになったら参考にしてみてください。

*1 従来の /org/gnome/Rhythmbox/Shell と /org/gnome/Rhythmbox/Player に相当するのは /org/gnome/Rhythmbox3 ではなく /org/mpris/MediaPlayer2

*2 xesam:title や xesam:artist、 xesam:album などが取得できる

*3 dbus-glibからGDBusへの移行については別の機会があれば説明することにします。

2020-08-20

Windows用デスクトップアプリケーションをクラウド上の使い捨て検証環境で検証する

Mozillaサポートに従事している結城です。

当社のFirefox/Thunderbirdサポートは多数のお客さまにお使い頂いていますが、インストーラの設定やWindowsのバージョンなどはお客さまによってまちまちです。検証環境をどうやって整えるかは、受託業務では無視できないポイントです。

最近、当社のFirefox/Thunderbirdサポートの検証環境をクラウドに移行しました。この移行でやりたかったのは以下のことでした。

  • Windows 10のVMをクラウド上に置く。
  • 検証環境は常時すぐ動かせる状態で置いておくのではなく、必要に応じてオンデマンドで用意し、検証の度に使い捨てにする。
  • 検証環境は、起動した時点で必要な条件の整備やテストケースなどの準備が一通り完了している。

この記事では、これらをどのように実現したのかをご紹介します。非常に長い記事になってしまいましたので、休み休み読んで頂ければ幸いです。

(2020年8月21日 12時30分追記:当初、Azure上でWindows 10を使う場合の選択肢にWindows 10の買い切り型ボリュームライセンスを挙げていましたが、こちらは事実誤認でした。関連する記述を訂正しました。)

移行の動機

実際にやったことの解説の前に、そもそも何故移行をしたかったのかを簡単に説明します。

元々当社では長年、FirefoxとThunderbirdのサポート業務において、社内に設置されているデスクトップPCをホストマシンとしたVMや、物理的なWindows PCを検証環境として使っていました*1。この検証環境は、セットアップ手順が自動化されていないことから、一度作った環境をずっと使い続ける運用になってしまい、その結果以下のような問題が起こっていました。

  • 前回検証時の影響が次回検証時に想定外の影響を及ぼしてしまう。*2
  • 1台の物理PCでは複数のお客さま向けの検証を並行して行えない。*3

それでもこの運用が継続していた最大の理由は、検証環境をガッツリ使用する機会が、Firefox ESR*4やThunderbirdのメジャーアップデートのタイミングに集中していたからです。その時期が過ぎると、検証環境は散発するお客さまからのお問い合わせを承けてのタイミングにしか使われないため、「まあ、1年の中でごく限られた時期くらいのことだし……」と見過ごされてきていました。

しかし、コロナウィルス感染拡大防止のためにリモート勤務が主体になった結果、この「1年に1回の限られた時期」すらも乗り切れない恐れが出てきました。前述の問題に加え、さらに以下の問題が表面化してきたからです。

  • VMホストをメンテナンスしにくい。*5
  • インターネット越しに社内ホストのVMを操作するときの操作性が劣悪である。*6
  • 物理PCをそんなにたくさん作業者の自宅に持ち込めない。

冒頭に挙げた「クラウドで」「使い捨てで」「起動時点で検証の準備が整った」検証環境が欲しいというニーズは、このような背景から生じたものです。

費用の見積もり

環境のセットアップをソフトウェアで完結できて便利なのは間違いありませんが、条件次第ではクラウドは割高になるともいいます。クラウドを使う場合と、物理PCあるいはレンタルサーバーでVMホストを調達する場合、どちらがコスト的に有利なのでしょうか。

当社のFirefox/Thunderbirdサポートのお客さまの多くは、現在はWindows 10をクライアントとして使われています。よって、「Windows 10を使う」ということを前提に費用を検討してみることにします。

クラウドの場合
ライセンス費用

結論を先に述べると、クラウドで検証目的のために手軽にWindwos 10を使うには、「Azureで、Windows 10 Enterprise E3/E5/A3/A5のボリュームライセンス、もしくはVisual Studioサブスクリプションを買う」ことになります。以下、公式のドキュメントであるWindows 10 on クラウドのライセンス認証についてWindows 10 サブスクリプションのライセンス認証から得られる情報に基づいて、詳しく述べていきます。

まず「Azureで」という点についてですが、これは以下の理由から、「クラウドで手軽に」という条件を満たせる選択肢が実質的にAzureのみとなるためです。

  • Azure以外のクラウドで使う場合、技術的な条件ではなく利用許諾の条件として、そのクラウドがQMTHの認証を承けている必要がある。
  • IIJdocomoKDDIなどがQMTH認証を得ているが、AWSやGoogle CloudはQMTH認証を得ていない。
  • 後述するTerraformの対応サービス一覧には、これらのQMTH認証クラウドが載っていない*7

また、「Windows 10をインストールして動かす権利」と「AzureでWindows 10を動かす権利」も別になっていて、前者だけあっても後者がないとAzure上ではWindows 10を使えません。後者の権利は、Windows 10 Enterprise E3/E5のボリュームライセンスの一部として含まれるほか、Visual Studioサブスクリプションの特典に含まれている*8ため、このいずれかを購入する必要があるということになります。

ボリュームライセンスとは、Windowsのライセンスを3つ以上まとめて購入する場合に取れる選択肢です。マシン1台あたりにすると1~2万円の買い切りになるようです。Windows 10をクラウド上で使える権利が含まれるライセンスは、買い切りタイプの一般的なWindows 10ライセンスではなく、サブスクリプション形式のWindows 10 Enterprise E3/E5のライセンスになるそうで、Windows 10 Enterprise E3の場合は月額760円、年間で1万円弱になるようです。検証環境の数だけライセンスを購入すると、このN倍が毎年の固定費増となります。

VSサブスクリプションは、開発者・検証作業者向けの選択肢です。VSサブスクリプションの「標準サブスクリプション」プランには検証用としてMicrosoft製品のライセンスが付属していて、本稿執筆時点のレートではVSサブスクリプション1ユーザーにつき初年度約12~3万円*9、2年目以降は8万5千円*10ほどで、使うか使わないか・使用頻度に関わらず毎年の固定費増となります。検証用に使えるWindows 10のライセンス数は限りが無いので、検証環境の数が多い場合はこちらの方が割安になります。

ライセンス費用以外の費用

Azureの場合、費用はVMが動作している時間に対してだけでなく、仮にVMが終了していても、ストレージ領域を消費していれば、VMの状態に関わらずその容量に応じて費用が発生します。

当社では他の事業ですでにAzureを使用しており、重たい処理を長時間実行させるようなハードな使い方をして、月あたり1案件で1~2万円程度に収まっているという実績がありました。Firefox/Thunderbirdサポートの検証では、そのようなハードな使い方はまずしませんし、そもそも冒頭に述べたニーズのとおりの「検証するときだけ作成して使い捨てにする」運用であれば、費用はもっと下がります。

また、先述のVSサブスクリプションにはAzureのクレジットが毎月6千円分付与されるという特典があります*11。VSサブスクリプションとAzureの組み合わせで使用した場合、当社のFirefox/Thunderbirdサポートでの使い方であれば、Azureの使用自体にかかるコストは、この特典のクレジット分で十分にまかなえると見込まれます(実際、検証用のVMを1つ丸1日使用してもコストは100円程度でした)。

クラウドでない場合(物理ホストまたはレンタルサーバー)
ライセンス費用

Windows 10プリインストールのPCを調達する場合、Windows 10のライセンスはハードウェア購入費の中に含まれる形になります。

VMホストを調達してそこにWindows 10のVMを立てる場合や、既存のハードウェアを流用してWindows 10をインストールする場合は、Windows 10のライセンスの調達の必要があります。この場合、Windows 10のライセンス購入やVSサブスクリプション契約の費用がかかります。

ただし、Azureの場合と異なり、こちらのケースではサブスクリプション形式のWindows 10 Enterprise E3/E5は必要なく、買い切りのライセンスが使えます。Windows 10 Professionalの場合、定価は3万円弱ですが、ボリュームライセンスで購入するともう少し安くなるのではないかと予想されます。

また、VSサブスクリプションの2年目以降の契約も、こちらのケースでは必須ではありません。VSサブスクリプションの特典で付与されたライセンスは契約終了後も永続的に使えるため、初年度のみ契約して2年目以降は継続しないというやり方を取ることができます。

ライセンス費用以外の費用

PCを購入するのであればその代金が、レンタルサーバーを借りるのであればその費用が必要です。これらはすべて、使うか使わないかに関わらず発生する固定費となります。

また、「そうして調達したPCやレンタルサーバー自体の運用」のコストも発生します。作業者目線では「自分が作業すればいいだけじゃないか」と感じるところですが、「工数単価の高い技術者に対して、検証環境の運用のためだけに拘束される時間が発生し、他の作業に割り当てられたはずの時間が圧迫される」のは、会社的にとっては損失になるということです。

どうするのが一番得か?

考慮するべき要素はいくつかあります。例えば以下の要領です。

  • リモート勤務はどのくらい継続するのか? リモート勤務が長期的に継続するのであれば、オフィスにいないと使えない物理ホストでは不便なので、割高であったとしてもクラウドを選択した方がよい。
  • その環境をどのくらい継続的に使うのか? 長く使うのであれば、ハードウェアを買って使い倒すのが最も割安と言える。検証対象が頻繁に変わるのであれば、柔軟性が高いクラウドの方が適している。
  • 作業者の工数単価はどのくらいか? 物理ホストやレンタルサーバーの運用に要する人的コストと、その人を何らかの案件にアサインして得られる収益を比較して、前者が上回るのであればクラウドを、後者が上回るのであれば物理ホストを選択するのが適している。
  • 検証対象の環境の種類は均一か? 検証環境の種類が多い場合、資産管理が煩雑になるため、「検証環境を作ってすぐ使い捨てにする」運用を取りやすいクラウドが適している。
  • 検証環境の数はどのくらいか? 検証環境の数が非常に多くなる場合、ボリュームライセンス購入よりVSサブスクリプションの方が割安となる。

諸々の条件を鑑みて、当社では「VSサブスクリプションを購入してAzureを使う」ことにしました。

使い捨ての検証環境の作り方

Terraform(テラフォーム)というツールを使うと、静的な設定ファイルに基づいてAzure上に所定の設定でVMや仮想ネットワークを構築・破棄できます。このブログでも過去に全3回にわたってTerraformの基本的な使い方の解説記事を掲載しました(1, 2, 3)。実のところ、今回の検証環境の移行はこれらの記事が発端となってのことでした。

当社で使用している検証環境設定用のレシピのテンプレート

これらの手順を元に作成した、当社のFirefox/Thunderbirdサポート業務での検証環境として必要な諸々の初期設定を行うように調整した main.tf および関連ファイル一式を公開しています本稿執筆時点のリビジョン)。基本的な使い方は以下の要領です。

  1. このブログのTerraformに関する連載のその1に従って、必要なコマンドをインストールして認証情報を取得し、~/.bashrc などに以下の内容を追加しておく、

    export SUBSCRIPTION_ID="********-****-****-*****************"
    export ARM_SUBSCRIPTION_ID="$SUBSCRIPTION_ID"
    export ARM_CLIENT_ID="********-****-****-*****************"
    export ARM_CLIENT_SECRET="**********************************"
    export ARM_TENANT_ID="********-****-****-*****************"
    
  2. これらのファイルがあるディレクトリに cd して makemake apply )を実行する。

    • もしAnsibleがエラーで実行完了できなかった場合は、make apply-playbook でAnsibleのみ再実行する。
  3. rdp/add_password.bat の位置に出力されたバッチファイルをWindows上で実行し、当該ディレクトリに保存されているRDPファイル*12に認証情報を付与する。

  4. 管理者権限を持たない一般ユーザー(rdp/ユーザー.rdp)または管理者ユーザー(rdp/管理者.rdp)としてログインし、検証を実施する。

    • 管理者パスワードが必要になった場合、password.txt または terraform.tfvars の中に出力されているものを使う。
  5. 検証が完了したら、make destroy を実行して検証環境を破棄する。

    • もしterraformがエラーで実行完了できなかった場合は、リソースグループ等がAzure上に残留しているため、Azureのダッシュボードから手作業で削除する。また、make clean でローカルの不要なファイルも削除する。

筆者が実行したときは、make の実行からVMが利用可能な状態になるまでに約10分を要しました。後述の日本語化を行う場合は、日本語パックのインストールに10分ほど時間がかかるため、make の実行からVMが利用可能になるまでの待ち時間は約20分強となります。このくらいの待ち時間であれば、「必要なときに起動して、用が済んだら破棄」というカジュアルな使い方も、無理なくできるのではないでしょうか。

ここからは、このテンプレートをどのようにして作成したかを順を追って解説してみます。

なお、ここからの内容は以下の3つの記事の内容を履修済みという想定で記述します。TerraformもAzureも初めて使うという方は、まずチュートリアルとして、これらを完遂する所まで実践しておいて下さい。

VMイメージの選択

過去記事の例ではWindows Serverを使用していますが、今回使いたいのはデスクトップPC向けのWindows 10です。

Terraformの設定に記述するイメージ名は、「Windows 10 2004」のような書き方はできず、内部的な名前で指定しないといけません。どんな値を設定すればよいかは、az コマンドでイメージの一覧を取得して調べる必要があります。以下は、デスクトップPC向けのWindowsのVMイメージの詳細な一覧をJSON形式で取得する例です。VMイメージ一覧の問い合わせには時間がかかるため、結果はローカルに保存しておくのがお薦めです。

$ az vm image list --publisher MicrosoftWindowsDesktop --all | jq . > vm-list-windows-all.json

Windows 10のイメージは、この結果の中に以下のような形で出現します。

  {
    "offer": "Windows-10",
    "publisher": "MicrosoftWindowsDesktop",
    "sku": "19h1-ent",
    "urn": "MicrosoftWindowsDesktop:Windows-10:19h1-ent:18362.959.2007101755",
    "version": "18362.959.2007101755"
  },

Windowsのバージョンとエディションは、この中の sku *13の値で区別します。先頭部分はコードネームの短縮形、その後にエディションや補足情報が続いており、以下のような意味になっています。

skuの値の例 コードネーム / Windowsのバージョン エディション 補足
rs1-enterprisen-g2 Redstone 1 (RS1) / Windows 10 1607 Enterprise N(Media Player削除版)、第2世代仮想マシン版
rs4-pro-zh-cn Redstone 4 (RS4) / Windows 10 1803 Professional 中国語版
rs5-enterprise-g2 Redstone 5 (RS5) / Windows 10 1809 Enterprise 第2世代仮想マシン版
19h1-ent 19H1 / Windows 10 1903 Enterprise
19h2-pro 19H2 / Windows 10 1909 Professional
20h1-evd 20H1 / Windows 10 2004 Enterprise マルチセッション(旧称:Enterprise for Virtual Desktops)

先のテンプレートでは、sku の初期状態は 19h1-pro 、つまり「Windows 10 1903 Professional」と指定しています。

なお、「起動時点で検証の準備が整った」検証環境を用意するという観点では、あらかじめ検証環境として準備を整えたVMのイメージをスナップショットとして取っておく方法もあります。ただ、今回の用途の前提では、

  • スナップショットがあっても、仮想ネットワークなどはその都度用意する必要がある。
  • スナップショットを保存するためのストレージ領域の保持に費用が発生する。検証を行っていない時間の方が圧倒的に長いと、ほとんどの費用が無駄になる。
  • 顧客ごとにWindowsのバージョンや設定を変える必要があり、せっかくイメージを作成してもあまり省力化にならない。

といった具合であまり旨味がないことから、独自のイメージは作らないことにしました。

検証環境内の設定

TerraformはAzure上にVMを用意するだけでなく、そのVM上でのPowerShellスクリプトの実行など、ある程度の環境設定も行えます。しかし筆者はPowerShellに明るくないため、PowerShellだけで必要なことすべてを実現するのは難しそうです。幸い、先のTerraform導入解説記事のその3ではAnsibleを使える状態にする所までの手順が解説されていますので、以後の検証環境内の設定はAnsibleで行うことにします。

先の記事の通りに作業を進めた段階では、main.tf の最後にはAnsibleでの接続用のインベントリを出力する local_file の定義があるはずです。その直後に、以下のようにAnsibleのPlaybookを作成する記述を追加します。

resource "local_file" "playbook" {
  filename = "ansible/playbook.yml"
  content  = <<EOL
- hosts: windows
  tasks:
    # ここに環境設定のための指定を書いていく
EOL
}

この状態で terraform apply を実行すると ansible/playbook.yml の位置にファイルが出力されます。それに続けて ansible-playbook -i ansible/hosts ansible/playbook.yml と実行すれば、できあがったばかりのVMをAnsibleで設定できるという具合です。

ということで、ここから先はAnsibleのPlaybookの書き方の話となります。

UACの無効化

Windowsでは、管理者権限の確認を求められたとき、画面が暗くなって認証のダイアログが表示されます。この「画面が暗くなる」という部分がUACと呼ばれる機能で、攻撃者によるダイアログの偽装や内容の読み取りなどを防ぐ効果があるのですが、副作用として、パスワード入力欄へのコピー&ペーストができなくなるという制限が生じます。

通常運用であればUACは有効にしておくことが推奨されますが、検証環境ではそこまでの安全性は必要ありません。また、インストーラを何度も実行するような検証ではその都度認証を求められるため、パスワードをコピー&ペーストできないと恐ろしく効率が悪いです。そのため、セットアップ時点でUACを無効化しておくことにします。

UACは、Windowsのレジストリの HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System 配下にある PromptOnSecureDesktop の値を DWORD 型で 0 に設定すれば無効化できます。これは、Ansibleでは win_regedit ディレクティブで実現できます。main.tf 内のAnsibleのPlaybook定義の tasks: に以下のようにタスクを追記します。

  tasks:
    (略)
    - name: Allow copy and paste to the UAC dialog
      win_regedit:
        key: HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System
        value: PromptOnSecureDesktop
        data: 00000000
        datatype: dword
日本語のワークグループ名にする

過去のサポート事例の中で、Windowsのワークグループ名に日本語が含まれている場合にのみ起こるトラブルがありました。そのような状況を想定し、検証環境も日本語のワークグループ名にしておくことにします。ワークグループ名の変更は、Ansibleでは win_domain_membership ディレクティブで行えます。

  tasks:
    (略)
    - name: Set non-ASCII workgroup name
      win_domain_membership:
        domain_admin_user: "${var.windows-username}"
        domain_admin_password: "${var.windows-password}"
        state: workgroup
        workgroup_name: ワークグループ
VMを日本語化する

AzureのVMイメージからセットアップした環境は、表示言語は英語、使用地は米国地域になっています。メニュー表記や時刻、キー配列などの諸々の「日本語化」を、Ansibleで行います。

表示言語を日本語化するためには、日本語のメッセージ定義などを含んだいわゆる「言語パック」が必要となります。言語パックはインターネット経由でそれ単体をダウンロードできるようにはなっていないので、まず以下のようにしてファイルを用意します。

  1. VSサブスクリプションのダウンロードセンターで「Windows 10 Language Pack, version (VMのWindowsのバージョン)」を検索する。

  2. DVDイメージをダウンロードする。

  3. ダウンロードしたイメージを仮想ディスクとしてマウントする。

  4. ディスク内から、Microsoft-Windows-Client-Language-Pack_x64_ja-jp.cab のように、言語名「ja-jp」を含んでいて拡張子が「.cab」である名前のファイルを探す。

  5. 見つかったファイルを、AzureのホストからHTTPでダウンロード可能な状態にする。例えば、自社運用のownCloudに置いてダウンロード用のURLを発行する。

  6. ダウンロード用のURLを変数にバインドできるように、variable.tf に変数の定義を追加する。

    variable "windows-language-pack-url" {}
    
  7. terraform.tfvars で以下のようにして、5のURLを変数にバインドする。

    windows-language-pack-url = "https://owncloud.example.com/index.php/s/***************/download"
    

これで準備が整いました。後は、win_file でダウンロード先ディレクトリを作成して、win_get_url で言語パックのファイルをダウンロードし、win_shell のPowerShellスクリプトで言語パックをインストールして、win_reboot でVMを再起動するようにすればOKです。

  tasks:
    (略)
    - name: Prepare directory to download language pack
      win_file:
        path: C:\temp
        state: directory
    - name: Download language pack file
      when: not "${var.windows-language-pack-url}" == ""
      win_get_url:
        url: "${var.windows-language-pack-url}"
        dest: 'c:\temp\lp.cab'
    - name: Install language pack
      when: not "${var.windows-language-pack-url}" == ""
      win_shell: |
        $LpTemp = "c:\temp\lp.cab"
        Add-WindowsPackage -PackagePath $LpTemp -Online
        Set-WinUserLanguageList -LanguageList ja-JP,en-US -Force
        Set-WinDefaultInputMethodOverride -InputTip "0411:00000411"
        Set-WinLanguageBarOption -UseLegacySwitchMode -UseLegacyLanguageBar
        Remove-Item $LpTemp -Force
    - win_reboot:
      when: not "${var.windows-language-pack-url}" == ""

各タスクに when: not "${var.windows-language-pack-url}" == "" と条件を指定しておくと、言語パックのURLが指定されていれば日本語化し、そうでなければタスクをスキップすることができます。言語パックのインストールだけで10分ほど時間がかかるので、必要がないのれあれば日本語化しないままで検証してもよいでしょう。

地域設定の変更は、タイムゾーンを変更する win_timezone 、地域を変更する win_region 、任意のPowerShellスクリプトを実行する win_shell 、Windowsを再起動する win_reboot の各ディレクティブを組み合わせて以下のように行います。

  tasks:
    (略)
    - win_timezone:
        timezone: Tokyo Standard Time
    - name: Set location
      win_shell: Set-WinHomeLocation -GeoId 0x7A
    - name: Set UI language
      win_shell: Set-WinUILanguageOverride -Language ja-JP
    - name: Set system language
      win_shell: Set-WinSystemLocale -SystemLocale ja-JP
    - name: Set date/time format
      win_shell: Set-WinCultureFromLanguageListOptOut -OptOut $False
    - name: Set keyboard layout
      win_shell: Set-ItemProperty 'registry::HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\i8042prt\Parameters' -Name 'LayerDriver JPN' -Value 'kbd106.dll'
    - win_reboot:
    - name: Set region globally
      win_region:
        copy_settings: yes
        location: "122"
        format: ja-JP
        unicode_language: ja-JP
    - win_reboot:

地域設定の変更は、言語パックをインストールしていなくても行えます。

管理者用のアカウントと一般用のアカウントを、日本語の名前で作成する

過去のサポート事例の中で、Windowsのユーザー名に日本語が含まれている場合にのみ起こるトラブルがありました*14。そのような状況を想定し、検証環境も管理者と一般ユーザーの両方で日本語のユーザー名のアカウントを作成することにします。これは、Ansibleでは win_user ディレクティブで以下のように行えます。

  tasks:
    (略)
    - name: Create administrator user
      win_user:
        name: "管理者"
        password: "${var.windows-password}"
        password_never_expires: true
        state: present
        groups:
          - Administrators
          - Users
          - Remote Desktop Users
    - name: Create regular user
      win_user:
        name: "ユーザー"
        password: "${var.windows-password}"
        password_never_expires: true
        state: present
        groups:
          - Users
          - Remote Desktop Users

あくまで検証用ということで、パスワードはどちらもVM作成時の物を流用しています。また、パスワードの有効期限も無効化しています。

管理者か一般ユーザーかの違いは、ユーザーがどのグループに所属しているかで変わります。group: での所属グループの指定に Administrators が列挙されていれば管理者、そうでなければ一般ユーザーとなります。また、検証をリモートデスクトップ接続で行えるようにするため、ここではそれぞれを Remote Desktop Users グループに所属させて、リモートデスクトップを使える状態にしています。

各ユーザーでリモートデスクトップ接続するためのrdpファイルを自動生成する

Azureのポータルからは作成したVMに接続するためのrdpファイル(リモートデスクトップ接続クライアント用の設定ファイル)をダウンロードできますが、このときのログオンユーザーはTerraformの操作用に作成したユーザーとなります。Ansibleで作成したユーザーでログインしたい場合は、明示的にユーザーを切り替えなくてはなりません。

毎回手動でログインユーザーを切り替えるのは煩雑なので、「管理者」ユーザーと「ユーザー」ユーザーでログオンするよう設定したrdpファイルも自動生成するようにしましょう。rdpファイルはプレーンテキストに所定の内容を出力すればそれで機能するので、main.tfの末尾に以下のように定義を追加します。

resource "local_file" "admin_rdp_shortcut" {
  filename = "rdp/管理者.rdp"
  content  = <<EOL
full address:s:${data.azurerm_public_ip.firefoxverify.ip_address}:3389
prompt for credentials:i:0
administrative session:i:1
username:s:管理者
EOL
}

resource "local_file" "user_rdp_shortcut" {
  filename = "rdp/ユーザー.rdp"
  content  = <<EOL
full address:s:${data.azurerm_public_ip.firefoxverify.ip_address}:3389
prompt for credentials:i:0
administrative session:i:1
username:s:ユーザー
EOL
}

このとき気をつけないといけないのが、password:s:(平文パスワード)のように書いてもパスワード入力は省略できないという点です。セキュリティの都合上、rdpファイルに含めるパスワード情報は暗号化する必要があります。

一連の暗号化にはPowerShellを使わないといけませんが、Terraformで「VM上ではなく、Terraformを実行しているローカル環境の上で」PowerShellスクリプトを動かす方法を、筆者は見つけられませんでした*15。幸い、筆者はWindows 10のWSLでTerraformを実行しているので、Windows用のバッチファイルを出力させてそれを手動で実行する、という形で解決するようにしてみました。

resource "local_file" "batch_to_add_password_lines_for_rdp_shortcuts" {
  filename = "rdp/add_password.bat"
  content  = <<EOL
@echo off
for /f "usebackq delims==" %%i IN (`dir *.rdp /b`) do powershell.exe -command "'password 51:b:' + (('${var.windows-password}' | ConvertTo-SecureString -AsPlainText -Force) | ConvertFrom-SecureString);" >> %%i
del add_password.bat
EOL
}

この指定によって生成されるバッチファイルを実行すると、rdpファイルの末尾に暗号化されたパスワードの情報が追記され、バッチファイル自体は用済みという事で消去されます。

各ユーザーのログオン後の初期状態を設定する

検証ではユーザープロファイルや設定ファイルを取り扱う場面がありますが、そういう場面ではWindowsの初期設定の「隠しファイルや拡張子は表示しない」という動作が煩わしいものです。検証環境のセットアップの一環として、最初からこれらの設定を「表示する」に変更しておきます。

これはWindowsのレジストリで HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced 配下の HiddenHideFileExt を設定すればよいのですが、ここで1つ問題があります。先に紹介した win_regedit ディレクティブを使うと、HKCU がAnsible実行時のユーザーの物になってしまって、先ほど用意したユーザーの設定を変えられないのです。

この問題を回避するためには、become_userwin_command を併用します。前者はいわゆる runas *16を行うためのディレクティブで、後者は任意のコマンド列を実行するディレクティブです。まず、runas できるようにAnsibleのPlaybookの冒頭に以下の通り記述を追加します。

- hosts: windows
  become_method: runas                                 # ←追加
  vars:                                                # ←追加
    ansible_become_password: "${var.windows-password}" # ←追加
  tasks:
    (略)

次に、実際にレジストリの値を設定するタスクを以下のように追加します。

  tasks:
    (略)
    - name: Show hidden files for the administrator user
      become: yes
      become_user: "管理者"
      win_command: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v Hidden /t REG_DWORD /d 1 /f
    - name: Show file extensions for the administrator user
      become: yes
      become_user: "管理者"
      win_command: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v HideFileExt /t REG_DWORD /d 0 /f
    - name: Show hidden files for the regular user
      become: yes
      become_user: "ユーザー"
      win_command: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v Hidden /t REG_DWORD /d 1 /f
    - name: Show file extensions for the regular user
      become: yes
      become_user: "ユーザー"
      win_command: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v HideFileExt /t REG_DWORD /d 0 /f

become: yesbecome_user: "コマンドを実行するユーザー名"win_command と併せて指定することで、そのコマンドが指定のユーザーで実行されるようになります。これによって、各ユーザーの HKCU 配下のレジストリ値を変更できる訳です。

この方法で、各ユーザーの言語設定も変更できます。以下のように、PowerShell を使って日本語に切り替えておきます。

  tasks:
    (略)
    - name: Set display language for the administrator user
      become: yes
      become_user: "管理者"
      win_shell: |
        Set-WinUILanguageOverride -Language ja-JP
        Set-WinDefaultInputMethodOverride -InputTip "0411:00000411"
    - name: Set display language for the regular user
      become: yes
      become_user: "ユーザー"
      win_shell: |
        Set-WinUILanguageOverride -Language ja-JP
        Set-WinDefaultInputMethodOverride -InputTip "0411:00000411"
検証用のWindowsアプリをインストールする

検証環境は素のWindowsなので、テキスト編集は「メモ帳」や「ワードパッド」などを使う必要があります。しかし、編集対象のファイルの文字エンコーディングや改行コードによっては、これらを使えない場合があります。できれば、EmEditorのようにもう少し高機能なテキストエディタを使える状態にしておきたい所です。どうすればよいのでしょうか?

Linuxディストリビューションであれば aptdnf などのコマンドでアプリケーションをインストールできますが、今の所Windowsで標準的にそのようなことを行うコマンドはありません。ただ、有志の運営のパッケージリポジトリを使ってそれを可能にしようというプロジェクトはあり、Chocolateyもその1つです。

Ansibleでは win_chocolatey というディレクティブを使ってChocolateyリポジトリからWindowsアプリをインストールできます。EmEditorもこのリポジトリに掲載されていますので、これを使ってEmEditorをあらかじめインストール済みの状態にしておきます。

  tasks:
    (略)
    - name: Setup chocolatey
      win_chocolatey:
        name: chocolatey
        state: present
    - name: Install EmEditor
      win_chocolatey:
        name: emeditor
        state: present
        allow_empty_checksums: yes
        ignore_checksums: yes

ここでは win_chocolatey を2回指定していますが、1回目はChocolatey自体のインストール、2回目がEmEditorのインストールとなっています。

使いたいアプリケーションがScoopの方で利用可能なのであれば、win_chocolatey の代わりに win_scoop を使って下さい(こちらは未検証です)。

Ansibleのモジュールでインストールできないアプリのインストール

ChocolateyやScoopなどのリポジトリに収録されていないアプリは多数あります。そのようなアプリは、ファイルのダウンロードや展開などの操作をAnsibleのタスクとして自分で記述する必要があります。

例えば、Windowsアプリが認識するシステムの時刻をアプリ単位で変更する「HookDate」もその1つです。Azure上のVMのシステムの時刻はホストマシン(ユーザーからは見えない)の時刻に強制的に同期されてしまうため、時刻や日付が絡む検証では重宝します。これをインストールする指定は、ファイルをダウンロードする win_get_url と、ダウンロードしたzip形式のアーカイブを展開する win_unzip とを組み合わせて、以下の要領で行えます。

  tasks:
    (略)
    - name: Download HookDate to override system time for Firefox
      when: not "${var.hookdate-download-url}" == ""
      win_get_url:
        url: "${var.hookdate-download-url}"
        dest: 'C:\Users\Public\hookdate.zip'
    - name: Extract contents
      when: not "${var.hookdate-download-url}" == ""
      win_unzip:
        src: 'C:\Users\Public\hookdate.zip'
        dest: 'c:\Users\Public'
        delete_archive: yes

ここでは、言語パックの場合と同様に、ダウンロードするファイルのURLを hookdate-download-url という変数で指定するようにしています。ダウンロード可能な位置へのファイルの設置と、variable.tf での変数定義、terraform.tfvars でのURLのバインドを行っておくことで、検証で使う必要が無い場合はタスクをスキップするようにしました。

テストケースの用意

この記事で用意している検証環境は、Firefoxサポートでの導入支援やカスタマイズ結果の検証に使うことを意図した物なので、検証時には共通のテストケースを使う場面が多いです。そこで、テストケースもダウンロードしておいて使いやすい位置に展開することにします。これは、前項同様に win_get_url でのダウンロードと win_unzip で行えます。

  tasks:
    (略)
    - name: Download firefox-support-common for testcases
      win_get_url:
        url: "https://github.com/clear-code/firefox-support-common/archive/master.zip"
        dest: 'c:\Users\Public\firefox-support-common.zip'
    - name: Extract contents
      win_unzip:
        src: 'c:\Users\Public\firefox-support-common.zip'
        dest: 'c:\Users\Public'
        delete_archive: yes
    - name: Extract only testcases
      win_copy:
        src: 'c:\Users\Public\firefox-support-common-master\testcases'
        dest: 'c:\Users\Public'
        remote_src: True
    - name: Delete needless files
      win_file:
        path: 'c:\Users\Public\firefox-support-common-master'
        state: absent

ダウンロードしたファイルを展開しただけだと階層が深くなってしまうので、ここでは win_copy で必要な分のファイルだけを取り出し、不要になった空ディレクトリを win_file で削除しています。

また、検証手順の中に「システムにインポートされたルート証明書をFirefoxが自動認識できているかどうか」を確かめる項目があります。テストケースの中には、通常は未知の認証局によって署名された証明書であるとしてエラーになるuntrusted-root.badssl.comについて、認証局の証明書をエンタープライズの証明書として配布した状態にするためのregファイルが含まれています*17。これは win_regmerge でインポート済みの状態にすることができます。

  tasks:
    (略)
    - name: Add enterprise cert for verification
      win_regmerge:
        path: 'C:\Users\Public\testcases\add-badssl-com-enterprise-root.reg'
よく参照するディレクトリへのショートカットの作成

検証においては、ユーザープロファイルを手動で削除して新規インストール直後の状態に戻したり、インストール先の構成ファイルを変更したりといった操作が度々発生します。そのような場面でよく開くことになるディレクトリへのショートカットを、作業時に使いやすいよう全ユーザー共通のデスクトップ上に作成しておくことにしました。これは win_shortcut で以下のようにして行えます。

  tasks:
    (略)
    - name: Create shortcut to Program Files
      win_shortcut:
        src: '%ProgramFiles%'
        dest: '%Public%\Desktop\Program Files.lnk'
    - name: Create shortcut to Program Files (x86)
      win_shortcut:
        src: '%ProgramFiles(x86)%'
        dest: '%Public%\Desktop\Program Files (x86).lnk'
    - name: Create shortcut to testcases
      win_shortcut:
        src: '%Public%\testcases'
        dest: '%Public%\Desktop\testcases.lnk'
    - name: Create shortcut to AppData
      win_shortcut:
        src: '%AppData%'
        dest: '%Public%\Desktop\AppData.lnk'
    - name: Create shortcut to LocalAppData
      win_shortcut:
        src: '%LocalAppData%'
        dest: '%Public%\Desktop\LocalAppData.lnk'
Hostsの設定

Firefoxのカスタマイズでは、特定のホストに対してサイト別設定をあらかじめ反映したり、セキュリティやプライバシー保護のための機能を特定のホストでだけ無効化したり、といった設定を行うことがあります。このカスタマイズの検証用として、本番環境と同じホスト名で実際にWebページを表示できる状態にしておくため、Hostsファイルを編集しておくことにします。これは win_hosts で行えます。

  tasks:
    (略)
    - name: Add popup blocker exception hosts for Security-4-5 and Security-4-6
      win_hosts:
        state: present
        canonical_name: hostname.example.com
        ip_address: '93.184.216.34'

なお、ここでホスト名を割り当てている 93.184.216.34 は、example.comの本稿執筆時点でのIPアドレスです。

まとめ

以上、サポート業務の上で必要となるWindows 10の検証環境について、クラウド上で使い捨てにする運用を取るために必要なコストの検討の例と、TerraformとAnsibleを使った「使い捨て前提の検証環境」の作成過程をご紹介しました。

クラウドやプロビジョニングツールは「開発環境の整備」や「運用中の状況の変化に合わせた迅速なスケールアウト」などを目的にWeb業界で使われる事が多いですが、「デスクトップアプリケーションの法人向け技術サポート」という分野でも、このように業務に役立てることができます。「なんとなく、今までずっとこうやってきたので、こうすると一番楽/安上がりだから……」で今となっては非効率的なやり方を継続してしまっていないか、やり方を改めるコストを過剰に高く見積もってしまっていないか、折に触れ再考してみてはいかがでしょうか?

*1 創業当時は今のような便利なクラウドサービスがなく、その頃からの運用をそのまま継続してきていた。

*2 検証の過程で実験的に行ったWindowsの設定変更の戻し忘れ、前回インストール時のレジストリ値が悪影響を及ぼす、など。

*3 FirefoxやThunderbirdのインストール先が既定の通りだと、設定ファイルが衝突してしまう。また、それを避けて別の位置にインストールしてしまうと、実際の使用環境との差異が無駄に増えてしまい、検証の妨げになる。

*4 法人向けの長期サポート版。

*5 オフィスにいないので、ディスク容量が不足してもディスクの設置作業ができない、ホストの電源が落ちたり無反応になったりしてしまってもすぐに再起動できない、など。

*6 事務所のインターネット回線が、一般家庭用と同程度のトラフィックを想定した契約内容のため、このような使い方ではボトルネックになってしまう。

*7 調べればこの中にもQMTH認証クラウド事業者があるのかもしれないが、そこまで調べ始めると「手軽に」という前提から外れてくるため、今回は未調査。

*8 「MSDN サブスクリプションをお持ちのお客様には、毎月 Azure の無料特典が提供されています。この無料特典で提供される Azure に限り、Windows 7/8/10 といったクライアントOSを開発・テスト目的でご利用いただける例外(*)があります。」との記述がある。「MSDNサブスクリプション」は「Visual Studioサブスクリプション」の旧称。

*9 契約上は1199ドル。

*10 契約上は799ドル。

*11 次月へは持ち越せず、使わなかった分は消失する。

*12 リモートデスクトップ接続用のショートカット。

*13 SKU: Stock Keeping Unit、在庫管理単位。Windowsではエディション構成にあたる。

*14 ホーム以下のパスに日本語が含まれるようになるため。

*15 調査が甘いだけの可能性はあるので、指摘をいただけると幸いです。

*16 Linux等での `sudo` に相当する。

*17 訪問してもエラーにならないことをもって、ルート証明書のインポートに成功していると判断する。

2020-08-21

Debianパッケージ利用調査に参加するとどうなるのか?

はじめに

Debianを対話的にインストールすると、Debianパッケージ利用調査への参加の可否がインストール途中で確認されます。

今回は、このDebianパッケージ利用調査とはどういう仕組みになっているのか、また、どのようなデータが送られることになっているのかについて説明します。

popularity-contestの仕組み

Debianパッケージ利用調査では、どんなパッケージがよく使われているのかを定期的にサーバーに送信するようになっています。
この役割を担っているのが、popularity-contestパッケージに含まれるプログラムです。

popularity-contestを有効にすると、cronで一定期間ごとにデータを収集して、popconサーバー へgzip圧縮したデータを送信するようになっています。

このときHTTP経由でデータを送信 *1 しますが、あらかじめこのデータは専用のGPG鍵(/usr/share/popularity-contest/debian-popcon.gpg)を使って暗号化しているものを送信しており、秘匿性を担保するようになっています。
あらかじめ、tor パッケージをインストールしていると、送信時に Tor を経由させることもできます。

なお既定ではこの送信する頻度は7.5日に1回です。こうして集められたデータは、 https://popcon.debian.org/ にて公開されています。

popularity-contestで送られるデータとは

popularity-contestの仕組みがわかったところで、実際にどんな情報が送られているのでしょうか。

実際に送られている情報は次のコマンドで収集しています。*2

/usr/sbin/popularity-contest --su-nobody

ここで収集したデータは、以下のフォーマットで出力されます。

POPULARITY-CONTEST-0 TIME:1597988034 ID:(HOSTID) ARCH:amd64 POPCONVER:1.70 VENDOR:Debian
...
1597968000 1588723200 libprotobuf22 /usr/lib/x86_64-linux-gnu/libprotobuf.so.22.0.4
...
END-POPULARITY-CONTEST-0 TIME:1597988093

上記をみるとなんとなく想像がつくかと思いますが、次のデータが含まれています。

  • 最終アクセス時刻
  • 最終変更時刻
  • パッケージ名
  • ファイル名

プライバシーの観点から、最終アクセス時刻は丸められています。

(HOSTID)は実際には自動的に生成された128bitのUUIDが含まれます。
ここで使用されるHOSTIDは /etc/popularity-contest.conf をみるとわかります。

注意すべきはカスタムパッケージをインストールしている場合(例えば、自社でパッケージングしたものなど)、popconデータに含まれます。
いまのところ、特定のパッケージだけ除外するような設定は用意されていません。

すぐに試してみるには

通常は不必要に送信しないようになっているので、 /var/lib/popularity-contest/lastsub に記録されている時刻を戻してあげると popularity-contest による利用調査の参加スクリプトを実行できるようになります。例えば、次のようにして時刻を戻してあげると試せるようになります。

echo $(echo $(date +%s) - 648000 | bc) | sudo tee /var/lib/popularity-contest/lastsub
1597149912
sudo /etc/cron.daily/popularity-contest 

popularity-contest 1.69以前では、 /var/lib/popularity-contest/lastsub ではなく、 /var/log/popularity-contest の最終変更時刻を戻してあげる必要があります。

複数のサーバーにデータを送信するには

既定では、http://popcon.debian.org/cgi-bin/popcon.cgi にデータを送信しますが、複数のサーバーに送信することもできます。

% cat /etc/popularity-contest.d/example.conf
KEYRING="$KEYRING --keyring /usr/share/keyrings/xxxxxxxx.gpg"
POPCONKEY="$POPCONKEY -r XXXXXXXXXXXXXXX"
SUBMITURLS="$SUBMITURLS http://exampl.com/popcon.cgi"

例えば、上記のような設定ファイルを /etc/popularity-contest.d 以下に配置すると、指定したGPG鍵束に含まれる鍵IDを使って暗号化した
データをgzip圧縮して http://exampl.com/popcon.cgi へ送信するということができます。
ここで、送信先に HTTPS を指定することはできません。

popularity-contestを再度有効にする方法について

ここまで読んで、インストール時に無効にしていたけど、Debianパッケージ利用調査に参加してもいいかな、と思えたら再度有効にしてみてください。次のコマンドで対話的に再度有効にできます。

sudo dpkg-reconfigure popularity-contest

まとめ

今回は、Debianパッケージ利用調査に参加すると何が起こるのか?どんな仕組みでデータが送られているのかについて説明してみました。
どれくらい広く使われているかというのは、そのパッケージをメンテナンスし続けるかどうかの一つの指標にもなりえます。
Debianインストール時に問答無用で不参加にするのではなく、参加することで自由なソフトウェアを使っていることを示してみるのはいかがでしょうか。

*1 HTTP経由で送信できなかった場合はデータをメールにて送信を試みるようになっています。

*2 popularity-contest 1.70の場合。1.67以前はまたちょっと違う。

2020-08-24

Windowsの新しいパッケージ管理システムのリポジトリにパッケージを登録するには

はじめに

クリアコードはFluentdの開発に参加しています。

Fluentdにはtd-agentというディストリビューションがあり、Treasure Dataというサービスにすぐに接続しに行けるプラグインをまとめてくれているパッケージやインストーラーが提供されています。
td-agentでは、td-agent 3からWindows向けのインストーラーを提供しており、Windowsへもインストールできます。

wingetとは

wingetとはMicrosoftが新しく開発したWindows向けのパッケージ管理システムです。

このwingetはフリーソフトウェアとして公開されており、また、パッケージを登録するリポジトリやプロセスも公開されています。
winget本体はmicrosoft/winget-cliリポジトリとして管理され、wingetのパッケージはmicrosoft/winget-pkgsとして公開されています。

wingetに登録するパッケージマニフェストを作成する

winget-pkgsのリポジトリをcloneしてきます。

PS> git clone git@github.com:microsoft/winget-pkgs.git

cloneしたwinget-pkgsにはTools以下のフォルダにYamlCreate.ps1という対話的にYAMLのマニフェストを作成するPowerShellスクリプトが用意されています。
また、winget-pkgsをforkしてoriginとしてリモートリポジトリを登録しておきます。

実際にマニフェストを作成してみる

実際にtd-agent 4.0.1のマニフェストを作成したログは以下の通りです。

PS> .\Tools\YamlCreate.ps1
Enter the URL to the installer: http://packages.treasuredata.com/4/windows/td-agent-4.0.1-x64.msi


Downloading URL.  This will take awhile...
Url: http://packages.treasuredata.com/4/windows/td-agent-4.0.1-x64.msi
Sha256: E9BEBBDB2FF488583DD779505A714C44560CA16499EFDCC571E1A14E18BD383C


File downloaded. Please Fill out required fields.
Enter the package Id, in the following format <Publisher.Appname>
For example: Microsoft.Excel: TreasureData.TDAgent
Enter the publisher: Treasure Data Inc.
Enter the application name: Treasure Agent
Enter the version. For example: 1.0, 1.0.0.0: 4.0.1
Enter the License, For example: MIT, or Copyright (c) Microsoft Corporation: Apache Lincense 2.0
Enter the InstallerType. For example: exe, msi, msix, inno, nullsoft: msi
Enter the architecture (x86, x64, arm, arm64, Neutral): x64
[OPTIONAL] Enter the license URL: https://www.apache.org/licenses/LICENSE-2.0
[OPTIONAL] Enter the AppMoniker (friendly name). For example: vscode: td-agent
[OPTIONAL] Enter any tags that would be useful to discover this tool. For example: zip, c++: fluentd, logging
[OPTIONAL] Enter the Url to the homepage of the application: https://www.fluentd.org
[OPTIONAL] Enter a description of the application: A data collector for Treasure Data.


Id: TreasureData.TDAgent
Version: 4.0.1
Name: Treasure Agent
Publisher: Treasure Data Inc.
License: Apache Lincense 2.0
LicenseUrl: https://www.apache.org/licenses/LICENSE-2.0
AppMoniker: td-agent
Tags: fluentd, logging
Description: A data collector for Treasure Data.
Homepage: https://www.fluentd.org
Arch: x64
Url: http://packages.treasuredata.com/4/windows/td-agent-4.0.1-x64.msi
Sha256: E9BEBBDB2FF488583DD779505A714C44560CA16499EFDCC571E1A14E18BD383C
InstallerType: msi
Yaml file created:  C:\Users\cosmo\Documents\GitHub\winget-pkgs\manifests\TreasureData\TDAgent\4.0.1.yaml
Now place this file in the following location: \manifests\TreasureData\TDAgent

この一連の対話的な操作により、td-agent 4.0.1のマニフェストが~\GitHub\winget-pkgs\manifests\TreasureData\TDAgent\4.0.1.yamlに作成されました。

動作確認をしてみます。

PS> winget validate .\manifests\TreasureData\TDAgent\4.0.1.yaml
マニフェストの検証は成功しました。
PS> winget install -m .\manifests\TreasureData\TDAgent\4.0.1.yaml
Found Treasure Agent [TreasureData.TDAgent]
このアプリケーションは所有者からライセンス供与されます。
Microsoft はサードパーティのパッケージに対して責任を負わず、ライセンスも付与しません。
Downloading http://packages.treasuredata.com/4/windows/td-agent-4.0.1-x64.msi
  ██████████████████████████████  24.0 MB / 24.0 MB
インストーラーハッシュが正常に検証されました
パッケージのインストールを開始しています...

winget validate /path/to/manifest.ymlwinget install -m /path/to/manifest.ymlも検証できたようです。

変更をコミットします。

PS> git checkout -b td-agent-4.0.1
PS> git add manifests/TreasureData/TDAgent/4.0.1.yaml
PS> git commit -s
[td-agent-4.0.1 545f751] Add Treasure Agent 4.0.1
 1 file changed, 18 insertions(+)
 create mode 100644 manifests/TreasureData/TDAgent/4.0.1.yaml

forkしたリモートリポジトリにpushします。

PS> git push origin td-agent-4.0.1
Counting objects: 6, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 837 bytes | 418.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
#...

winget-pkgsのリポジトリにマニフェストを登録する

forkしたリモートリポジトリに作成したマニフェストをpushしたらfork元のmicrosoft/winget-pkgsに対してPull Requestを作成します。

今回作成したマニフェストは microsoft/winget-pkgs#3153 として登録しました。

td-agentは現在署名をしていないパッケージなので、Microsoft Defender SmartScreenの警告に引っかかってしまうため少しマージまでに時間がかかりましたが、無事に取り込まれました。

microsoft/winget-pkgsにマニフェストが取り込まれてしばらくすると、マニフェスト情報が更新され、td-agent 4.0.1がwinget searchで引っかかるようになりました。

PS> winget search td-agent
名前             ID                   バージョン 一致
-----------------------------------------------------------
Treasure Agent TreasureData.TDAgent 4.0.1 Moniker: td-agent

インストールすることもできます。

PS> winget install td-agent
Found Treasure Agent [TreasureData.TDAgent]
このアプリケーションは所有者からライセンス供与されます。
Microsoft はサードパーティのパッケージに対して責任を負わず、ライセンスも付与しません。
Downloading http://packages.treasuredata.com/4/windows/td-agent-4.0.1-x64.msi
  ██████████████████████████████  24.0 MB / 24.0 MB
インストーラーハッシュが正常に検証されました
パッケージのインストールを開始しています...
インストールが完了しました 

まとめ

FluentdのWindows周りの作業はクリアコードが得意としている領域です。td-agentのインストールがより手軽にできるようになれば、結果的にWindowsでのFluentdやtd-agentのユーザーを増やす事ができます。
この取り組みはWindowsにおいてtd-agentをより簡易に導入できるようにする施策の一環として実施されました。

当社では、お客さまからの技術的なご質問・ご依頼に有償にて対応するFluentdサポートサービスを提供しています。Fluentd/Fluent Bitをエンタープライズ環境において導入/運用されるSIer様、サービス提供事業者様は、お問い合わせフォームよりお問い合わせください。

タグ: Fluentd
2020-08-25

DebConf20でバグトラッキングに関する発表をしました

はじめに

クリアコードの林です。ククログでDebian関係の記事をときおり書いています。
今回は、DebConf20で発表する機会があったのでその内容を紹介します。

DebConfについて

DebConfとは、年に一回Debian開発者があつまるカンファレンスです。*1
今年はCOVID-19の影響もあり、イスラエルでの開催は見送られ、オンラインでの開催となりました。
来年以降は2021年にイスラエル、2022年にコソボ、2023年はインドでの開催予定となっています。

An experiment about personalized front-end of bugs.debian.org

本発表は、Debianのバグトラッキングシステム(bugs.debian.org)に関連した内容です。
bugs.debian.orgの歴史は古く、やりとりはメールをベースとしています。そのため、バグの状態を変更するのもメールを送ります。*2

バグ報告や、バグを検索したりするのには bugs.debian.orgの検索用のインターフェースがありますし、他にもいくつかパッケージを普段メンテナンスしている人向けのサービスは充実しています。

  • Developer's Packages Overview メンテナンスしているパッケージのビルド状態やLintianの結果など品質の観点で一覧できるのが便利。
  • Bugs search 各種フィルタを適用してバグを検索できる。例えば、Release Criticalなバグを表示するのに便利。
  • Maintainer dashboard メンテナー向けのダッシュボードを提供。Todo Listを表示してくれるのが便利。
  • Debian Package Tracker 特定のパッケージの更新をメールで追いかけたいときに便利。

しかし、自分がとくにメンテナンスしていないパッケージの特定のバグを直したり、その後のバグの状態を追いかけたりするのがやや手間に感じていました。
そこで、既存のメールのアーカイブやudd.debian.org(Debianに関する各種データをあつめているデータベース)から取得できるデータや、インストール済みパッケージの情報などを組み合わせる実験をしてみました。(インストール済みパッケージの情報についてはpopconの既存の仕組みを流用するようにしてみました。popconがどのようなデータをサーバーに送るのかについては、Debianパッケージ利用調査に参加するとどうなるのか?として記事を書いているのでそちらを参照してください。)

これにより、Debian unstable(Debianの開発版)を普段使いつつ、バグを直したり、その後の状況を追いかけたり、影響を受けそうなバグに気づけたりする仕組みを実現できないかというのが発表の趣旨です。

いろいろと今後の改善点はありますが、Q&AやIRC(#dc20-talks)を見る限りでは、おおむね好意的な反応が得られました。(似たような取り組みを10年くらい前にやった人もいたようです。また、サーバーのリソースが必要なら調整してもらえそうな感触が得られました。)

発表したときの録画内容*3は、後日DebConfのビデオのアーカイブサイトもしくは、YouTubeのDebConf Videosチャンネルでおそらく公開されるはずです。過去のDebConfのアーカイブもあるので、そちらも見てみると新たな知見が得られるかもしれません。

まとめ

今回は、DebConf20がオンラインで開催されたこともあり、発表者として参加することにしました。
DebConf20は29日までの開催となっています。日本時間だと19:00ぐらいから翌朝までの時間帯なので参加しにくいものもありますが、ぜひスケジュールを確認し興味があったら参加してみてください。

*1 Debianパッケージの設定に関連した debconf(1)というのもありますが今回は関係ありません。

*2 コントロールコマンドをメール本文に記述します。

*3 今回は事前に録画編集したものを提出して、それを発表時間に流してもらう形式でした。Q&Aの時間にJitsiでやりとりしたり、Etherpadで記録を残すというのもありました。

2020-08-26

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|