無印吉澤

Site Reliability Engineering(SRE)、ソフトウェア開発、クラウドコンピューティングなどについて、吉澤が調べたり試したことを書いていくブログです。

Swim.com の workout データをエクスポートする方法

Swim.com 活用のすすめ

自分以外に使っている人を見たことないですが、Swim.com というスイマー用の SNS があります。

この SNS は、泳いだ距離や速さを自動的に記録できるスイムウォッチや、Swim.com アプリが動くスマートウォッチに対応しており、スイマー同士でスコアを競えるようになってます(参考:Swim.com に対応したスイムウォッチ一覧)。まあ、SNS ですけど、自分のデータをアップロードして、確認するためだけにも使えます。

僕は、去年の9月までは Pebble Time で水泳データを記録していたのですが、10月以降はより高機能な Garmin Swim というスイムウォッチに乗り換えました。Garmin Swim のデータは Garmin Connect というサイトで確認できるのですが、以前のタイムとも比較したいので、Garmin Connect のデータを Swim.com にも連携させています。

図にすると、以下のような感じです。

f:id:muziyoshiz:20160422003752p:plain

このように連携させることで、以下の Workout 画面(https://www.swim.com/my-workouts/)で、Pebble Time の測定値と Garmin Swim の測定値を一覧できます。

f:id:muziyoshiz:20160422002408p:plain

今回の目的

Swim.com で一覧表示はできるんですが、1回あたりに泳いだ距離や、泳ぐペースの変化をグラフ表示するような機能はありません。また、Garmin Connect には CSV エクスポート機能があるのですが、Swim.com の方には CSV エクスポート機能も API もなんにもありません。厳しい……。

異なるスイムウォッチで測定した結果をなんとかグラフ化したいと思い、Workout 画面のデータをエクスポートする処理を実装しました。

採用した方法:JSON を手作業でテキストファイルに貼り付けて、Ruby のスクリプトで CSV に変換

色々試したのですが、Swim.com はログイン機能が特殊で、単にユーザ名とパスワードを POST するだけではログインできそうになかったので、自動化は早々に諦めました。厳しい……。

その一方で、一度ログインしてしまえば、Workout 画面の情報は GET で簡単に取得できることがわかりました。Workout 画面は最新の10件のデータを表示し、ページの末尾に到達すると次の10件を読み込むのですが、内部的には以下の URL に GET でアクセスしていました。

https://www.swim.com/workout/listing/search?pageIndex=1&pageSize=10&orderBy=workoutdate&orderDir=DESC&_=1461254054552

ざっと試した感じでは、この pageSize を 10 から 1000 に変えれば 1000 件取得できるし、orderDir を DESC から ASC に変えれば昇順に取得できるみたいです。やさしい……。

というわけで、こんな手順でエクスポートすることにしました。

  1. Web ブラウザで Swim.com にログインする
  2. Web ブラウザで https://www.swim.com/workout/listing/search?pageIndex=1&pageSize=1000&orderBy=workoutdate&orderDir=ASC にアクセスする
  3. 2 で表示された JSON を swim_com.json にコピペする
  4. swim_com.json と同じディレクトリに swim_com_parser.rb を置いて、ruby swim_com_parser.rb を実行する
  5. パース結果が swim_com.csv に出力されるので、このファイルをよしなにする

swim_com_parser.rb は Gist で公開しておきました。ご参考ください。

JSON Parser for swim.com workout data

関連記事

qiita.com

Ansible でインベントリのホスト変数を when に渡した際の不思議な動作(Ansible 1.9/2.0 共通)

f:id:muziyoshiz:20160331232512p:plain:w300

Ansible を使っていて、不思議な動作に遭遇したので深入りしてみました。

今回の落とし穴

同じグループに属するホストのうち、特定のホストのみで実行したいタスクがある、ということがたまにあります。そういうとき、私はインベントリファイルでホスト変数を定義して、この変数で処理を分岐させる、ということをしていました。

例えば、Webserversグループのうち、Webサーバ1番のみで実行したいタスクがある場合、インベントリファイルを以下のように書きます。

[webservers]
web1.example.com var1=true
web2.example.com var1=false

そして、以下のように when ステートメントを使えば、web1 のみでコマンドが実行されます。

- command: /usr/bin/do_something.sh
  when: var1

しかし、ホストの追加時に変数 var1 の定義をさぼってしまうと、実行時に「var1 が未定義である」というエラーが発生し、playbook の実行が止まってしまいます。

[webservers]
web1.example.com var1=true
web2.example.com var1=false
web3.example.com

それならタスクの方で var1 の中身を調べる前に、var1 が定義済みかどうかを調べればよいのでは?と思い、タスクを以下のように書きなおしてみました。

- command: /usr/bin/do_something.sh
  when: var1 is defined and var1

しかし、これを実行すると、web1(var1=true)だけでなくて web2(var1=false)でもこのコマンドが実行される という問題が発生しました。えっ……? どういうこと?!

この問題の原因っぽい(でも原因の説明にならない)もの

この問題の原因っぽいものとして、「インベントリファイルの中で定義した変数は、すべて string として解釈される」という Ansible の仕様があります。

Values passed in using the key=value syntax are interpreted as strings. Use the JSON format if you need to pass in anything that shouldn’t be a string (Booleans, integers, floats, lists etc).
Variables — Ansible Documentation

しかし、これは when: var1 と書いた場合と when: var1 is defined and var1 と書いた場合で結果が違ってしまうことの説明にはなりません。

いろいろなパターンを試す

こうなってくると when 自体どこまで信用していいのか不安になってきたので、いろいろなパターンで動作検証してみました。また、Ansible 1 系から 2 系へのアップグレード中にこの問題に遭遇したため、Ansible 1.9.4 と 2.0.1 の両方で検証しました。

動作検証の方法

3種類のインベントリファイルを用意しました。違いは、変数 var1 のみです。

inventory_true

localhost var1=true

inventory_false

localhost var1=false

inventory_null

localhost var1=

これらのインベントリファイルを使って、色々な when ステートメントを記載した playbook を実行しました。タスクの詳細は↓をご覧ください。

ansible-2.0-sample/main.yml at master · muziyoshiz/ansible-2.0-sample

動作検証の結果

Ansible 1.9.4 と 2.0.1 の両方で、ほとんど同じ結果になりました。ただし、var1= と書いた場合は、Ansible 1.9.4 だと when: var1 の場合に限り、fatal エラーが出て異常終了しました。

when statement var1=true (1.9/2.0) var1=false (1.9/2.0) var1= (1.9) var1= (2.0)
when: var1 is defined ok ok ok ok
when: var1 ok skipping fatal skipping
when: "{{ var1 | bool }}" ok skipping skipping skipping
when: var1 == true skipping skipping skipping skipping
when: var1 == "true" ok skipping skipping skipping
when: var1 == false skipping skipping skipping skipping
when: var1 == "false" skipping ok skipping skipping
when: var1 is defined and var1 ok ok skipping skipping
when: var1 is defined and {{ var1 | bool }} ok skipping skipping skipping
when: var1 is defined and var1 == true skipping skipping skipping skipping
when: var1 is defined and var1 == "true" ok skipping skipping skipping
when: var1 is defined and var1 == false skipping skipping skipping skipping
when: var1 is defined and var1 == "false" skipping ok skipping skipping

この結果をざっくりまとめると、以下のようになります。

  • when: var1 と書いた場合は、var1 が文字列 "true" なら true、"false" なら false と解釈される。これは、普通の人が期待しそうな動作(と思う)。
  • when: var1 is defined and var1 と書いた場合は、var1 に文字列が何かしら設定されていれば、それが "true" でも "false" でも true と判定してしまう。
  • この奇妙な動作は、Ansible 2.0 へのバージョンアップが原因ではない。
  • {{ var1 | bool }} と boolean への変換を明示すれば、期待通りに動作する。
  • var1 を文字列 "true", "false" と比較すれば、期待通りに動作する。
  • その一方、var1 と、boolean 型の true, false との比較は、常に false になる。

true/false を True/False に変えた場合の動作(2016-04-01追記)

この件について GitHub で issue として報告してみたところ、true じゃなくて True(先頭が大文字)と書け、という返信をもらいました。

Inconsistent behavior of when statement · Issue #15218 · ansible/ansible

そこで、

inventory_large_true

localhost var1=True

inventory_large_false

localhost var1=False

を新たに用意して、改めて動作確認してみた結果はこちら。動作に一貫性がなさそうに見える箇所に (inconsistent) と書いています。

when statement var1=true var1=false var1=True var1=False
when: var1 is defined ok ok ok ok
when: var1 ok skipping (inconsistent) ok skipping
when: var1 | bool ok skipping ok skipping
when: var1 == "true" ok skipping skipping skipping
when: var1 == "True" skipping skipping skipping skipping
when: var1 == "false" skipping ok skipping skipping
when: var1 == "False" skipping skipping skipping skipping
when: var1 is defined and var1 ok ok (inconsistent) ok skipping
when: var1 is defined and var1 | bool ok skipping ok skipping
when: var1 is defined and var1 == "true" ok skipping skipping skipping
when: var1 is defined and var1 == "True" skipping skipping skipping skipping
when: var1 is defined and var1 == false skipping skipping skipping ok
when: var1 is defined and var1 == "false" skipping ok skipping skipping
when: var1 is defined and var1 == "False" skipping skipping skipping skipping

この結果を見る限り、ホスト変数として True/False と書くと boolean として解釈されるようです。また、文字列としての "True"/"False" とは一致しなくなりました。

こうなると、ホスト変数に true/false と書いた場合に、どうして when: var1 だけは変数を boolean として扱ってくれる(扱ってしまう)のか気になってきます。中途半端に型の概念が導入されていて、個人的にはなんだか気持ち悪くなってきました……。

結論:特定のホストのみで実行したいタスクがある場合、結局どうすればいいのか(2016-04-01修正)

インベントリファイルでホスト変数を定義するときに、True/False の先頭を大文字にしないと正しく動作しない こともある と認識した上で、playbook を以下のように書くのが良さそうです。

inventory

host1 var1=True
host2
host3

playbook

- command: /usr/bin/do_something.sh
  when: var1 is defined and var1

ただ、処理を分岐させたい場所すべてで、var1 is defined and という冗長な記載が必要になってしまうのが、嫌なところです。かといって、この記載をサボると、var1 の定義がないホスト(上記の host2, host3)で異常終了してしまいます。

代案としては、記載を簡潔にするために、変数定義の有無だけをフラグとして使う方法も考えられます。つまり、インベントリファイルには host1 var1=True と書くものの、playbook には

- command: /usr/bin/do_something.sh
  when: var1 is defined

と書いて、変数 var1 の中身は検査しないという方法です。ただ、この方法は、事情を知らない人には「var1=False と修正すれば動作を変更できる」と誤解される危険性があります。なかなか悩ましいところです……。

Ansible 2.0 で追加されたモジュールおよびオプション一覧を無理矢理作る

f:id:muziyoshiz:20160313233740p:plain:w640

Ansible 2.0 の売りの一つ:新モジュールの豊富さ

Ansible 2.0 Has Arrived(日本語訳:Ansible 2.0リリース!)のなかで、Ansible 2.0 は「200個以上の新モジュール」を同梱している、と謳われています。

Ansible 2.0 also continues our long “batteries included” tradition by including over 200 new modules. Ansible 2.0 Has Arrived

モジュールの一覧は All Modules (Ansible Documentation) にあります。個々のモジュールのページを開くと "New in version 2.0." などと書いてあるのですが、2.0 で追加されたモジュールの一覧は存在しないようです。

しかし、Ansible 2.0 に移行したら、せっかくだから 2.0 でしか使えないモジュールやらオプションを使ってみたいものです。そこで、このページをクロールして、Ansible 2.0 で追加されたモジュールとオプションを列挙してみました。

今回実装したクローラ

Ruby のライブラリ Anemone を使ったクローラを書きました。良い機会なので「Rubyによるクローラー開発技法」を一通り読んでから書いてみましたが、こちらは既存のライブラリで実現できることを一通り把握できる良い本でした。

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

Rubyによるクローラー開発技法 巡回・解析機能の実装と21の運用例

今回実装したクローラは、以下のような動作を行います。

  • All Modules (Ansible Documentation) からリンクされている、末尾が _module.html のファイルをすべて辿る
  • ページの冒頭に New in version 2.0. と書いてあれば、Ansible 2.0 の新モジュールと見なす
  • Options の表に、(Added in 2.0) と書いてあるパラメータがあれば、Ansible 2.0 の新オプションを含むモジュールと見なす
  • サイドバーの表示をチェックして、そのモジュールが属するカテゴリを判別する
  • クロールした結果を、Markdown 形式で出力する

このクローラのソースコードは Gist で公開しました。引数を変えれば、過去のバージョン(1.9 とか)で追加されたモジュールの一覧や、次の 2.1 で追加されるモジュールの一覧を作ることもできます。興味のある方は手元で動かしてみてください。

クローラのソースコード(Gist): Crawler to create a list of new modules in Ansible 2.0

クローラの実行結果

2016年3月20日現在、All Modules (Ansible Documentation) には495件のモジュールが掲載されています。

今回のクローラを実行した結果、Ansible 2.0 で追加されたモジュールは194個、以前からあるモジュールに追加されたオプションは155個でした。あれ、200個以上の新モジュールって話では……? まあ、大体合ってるからいいですよね。

結果をざっと見てみると、確かにクラウド関係のモジュール(Cloud Modules)が130個と、群を抜いて多かったです。他には expectfind なんていう、以前からありそうな名前のモジュールも、2.0 で新たに追加されたようです。Zabbix 関係のモジュールもありますね。

ざっと見てみて、使いたいモジュールが見つかったら、思い切って Ansible 2.0 に移行してみるのも良いかもしれません。その際は、以前に書いた Ansible 2.0 移行事例をご参考ください(職場ブログの宣伝)。

recruit.gmo.jp

以下、クローラのソースコードと、自動生成したモジュール、オプションの一覧です。目次ありの一覧を見たい方は、Qiita に同じ内容を貼っておいたので、そちらをご覧ください。

qiita.com

続きを読む

Ansible 2.0 から使えるようになった consul モジュールを試してみた

Ansible 2.0 になって使えるモジュールが大幅に増えましたが、その中に Consul 関係のモジュールがいくつかあります。前から気になってたので、ちょっと試してみました。

今回のお試し環境

ホストOS

  • OS X Yosemite 10.10.5
  • VirtualBox 5.0.10
  • Vagrant 1.8.1
  • Ansible 2.0.1

ゲストOS

  • CentOS 7.2

Playbook

昨年末に Web サーバ1台、MariaDB サーバ1台、Cloudera QuickStart VM で Consul クラスタを組む playbook を作ったので、今回はこれを使いました。

f:id:muziyoshiz:20160313235925p:plain:w640

Ansible 2.0 対応

以前作った playbook のままだと Ansible 2.0 での実行時に WARNING が出たので、事前に少し修正しました。

今回は 2.0 への移行については触れないので、そちらに興味のある方は、先週末に職場ブログに書いた記事をご参考ください。Ansible 2.0 への移行方法、移行時の注意点に関する詳細をまとめています。

recruit.gmo.jp

話はちょっとズレますが、Consul は yum で配布されていないので、以下のように zip を落として解凍して……という処理をいちいち書いてます。Ansible 2.0 にアップグレードしたので、block を使って少し書き換えました。

hashicorp-sample/ansible/roles/consul_agents/centos7/tasks/main.yml

# Install unzip
- name: Install unzip for unarchive module
  yum: name=unzip state=present

# Install Consul if not installed
- name: Check if consul is installed
  stat: path=/usr/local/bin/consul
  register: consul_bin
- block:
  - name: Download and unzip Consul {{ consul_version }} command
    unarchive: src="https://releases.hashicorp.com/consul/{{ consul_version }}/consul_{{ consul_version }}_linux_amd64.zip" dest=/home/vagrant copy=no

  - name: Add execution permission to consul command
    file: path=/home/vagrant/consul state=file mode="0755"

  - name: Move consul command
    command: mv /home/vagrant/consul /usr/local/bin/consul

  when: not consul_bin.stat.exists

# Create Consul agent common setting
- name: Create consul setting directory
  file: path=/etc/consul.d state=directory owner=root group=root mode=0755
- name: Create common consul setting
  template: src="consul.service" dest="/etc/systemd/system/consul.service" owner=root group=root mode=0644
# Reload files under /etc/systemd/system
- name: Call daemon-reload explicitly because service module does not call it
  command: systemctl daemon-reload
# Start consul and create autostart setting
- name: Autostart Consul
  service: name=consul state=started enabled=yes

Consul 関係のモジュール

Ansible 2.0 Has Arrived によると、Ansible 2.0 は200個以上の新モジュールを含む、とのこと。All Modules — Ansible Documentation を見る限り、Consul 関係では以下の4個が新たに追加されたようです。

今回は、一番使いそうな consul モジュールを試してみました。

consul モジュールで書き換えるとこうなる before/after

before

consul モジュールを使わない場合、Consul にサービスを追加する playbook はこんな感じになります。これは、何らかの管理機能を提供する Web サーバを、Consul にサービスとして登録する例です。

hashicorp-sample/ansible/roles/managers/tasks/main.yml

# Create Consul client settings
- name: Create manager service information for consul
  template: src="consul/manager.json" dest="/etc/consul.d/manager.json" owner=root group=root mode=0644
- name: Restart consul
  service: name=consul state=restarted

hashicorp-sample/ansible/roles/managers/templates/consul/manager.json

{
  "service": {
    "name": "manager",
    "port": 80,
    "check": {
      "script": "curl localhost:80 >/dev/null 2>&1",
      "interval": "15s"
    }
  }
}

after

単純に考えると、こう書けば OK そうな気がします。Before とは違って、テンプレートは不要になります。簡潔ですね。

- name: Register manager service
  consul:
    service_name: manager
    service_port: 80
    script: "curl localhost:80 >/dev/null 2>&1"
    interval: 15s

これを実行すると、こんなエラーが出て失敗します。

TASK [cdh_quickstart : Register manager service] *******************************
fatal: [cdh-quickstart]: FAILED! => {"changed": false, "failed": true, "msg": "python-consul required for this module. see http://python-consul.readthedocs.org/en/latest/#installation"}

consul モジュールのページを見て、最初はホストOSに python-consul を入れろということなのか?と誤解してしまったのですが、ゲストOSの方に入れないと駄目みたいです。python-consul のインストールには pip が必要なので、修正後は以下のようになります。

- name: Install pip
  yum: name=python-pip state=present
- name: Install python-consul
  pip: name=python-consul state=present
- name: Register manager service
  consul:
    service_name: manager
    service_port: 80
    script: "curl localhost:80 >/dev/null 2>&1"
    interval: 15s

これで python-consul のエラーは出なくなったのですが、今度は以下のエラーが出たり出なかったりするようになりました……。

TASK [managers : Register manager service] *************************************
fatal: [managers]: FAILED! => {"changed": false, "failed": true, "msg": "Could not connect to consul agent at localhost:8500, error was HTTPConnectionPool(host='localhost', port=8500): Max retries exceeded with url: /v1/agent/services (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0xef5650>: Failed to establish a new connection: [Errno 111] \\xe6\\x8e\\xa5\\xe7\\xb6\\x9a\\xe3\\x82\\x92\\xe6\\x8b\\x92\\xe5\\x90\\xa6\\xe3\\x81\\x95\\xe3\\x82\\x8c\\xe3\\x81\\xbe\\xe3\\x81\\x97\\xe3\\x81\\x9f',))"}

ローカルで動作する consul agent に接続できない、というメッセージに見えます。このタスクより前に agent は起動しているんですが……。色々試してみたところ、consul モジュールの実行前に agent を再起動すれば、このエラーは出なくなりました。

最終形はこんな感じ。

hashicorp-sample/ansible/roles/managers/tasks/main.yml

- name: Install pip
  yum: name=python-pip state=present
- name: Install python-consul
  pip: name=python-consul state=present
- name: Restart consul
  service: name=consul state=restarted
- name: Register manager service
  consul:
    service_name: manager
    service_port: 80
    script: "curl localhost:80 >/dev/null 2>&1"
    interval: 15s

この hashicorp-sample は dnsmasq の設定も行っているので、$ dig @127.0.0.1 -p 8600 impala.service.consul SRV と問い合わせると IP アドレスが返されます。また、この manager の Nginx を停止させると、IPアドレスが返されなくなります。

まとめ

pip と python-consul のインストールを要求される割に、consul モジュールを使わない場合と比べて、それほど簡潔な表記にはなりませんでした。1台のマシンで複数のサービスを起動するような場合には、テンプレートファイルを減らすことができるので、それがメリットでしょうか?

あと、これはユースケースによっては致命的だと思うのですが、 consul モジュールで登録したサービスは consul agent を再起動すると消えてしまいます 。実サービスで使う場合、consul agent が落ちるたびに playbook を最初から実行しなおし、という想定なのでしょうか。また、今回のように Vagrant の provisioner として Ansible を使う場合も、playbook は(明示しないと)初回起動時にしか実行されないので、これはちょっと不便ですね。

とりあえず動かすことはできたものの、個人的にはこのモジュールの使いどころがちょっと良くわからないな……という感想でした。