aboutsummaryrefslogtreecommitdiff
path: root/posts/guile-df.md
blob: 391ef3544b0f41758a0fd30d24382dfef307a680 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
title: Guile で df コマンドの出力からディスク使用率を確認してみた
id: guile-df
date: 2021-09-12 00:15
description: Guile で Unix のコマンドの出力を一行づつ読んで処理する具体的な例を紹介します
---

## はじめに

[Guix System](https://guix.gnu.org/) で動かしているサーバーのディスク容量を監視するために
[Guile](https://www.gnu.org/software/guile/) で `df`
コマンドの出力を読んでディスク容量を確認する手続きを
Guile で作成したので紹介します。本記事によって Guile
でコマンドの出力を一行づつ読んで処理するときのイメージが掴めると思います。なお、本プログラムは異常時の処理を書いていない雑なものなので参考にする場合は注意をお願いいたします。



## 本文中のプログラムのライセンス

本記事で例示するプログラムのライセンスは GPLv3 (or later) です。ライセンスの詳細は
[monitoring](https://git.tojo.tokyo/monitoring.git/) リポジトリの
[COPYING](https://git.tojo.tokyo/monitoring.git/tree/COPYING) ファイルと
[`tojo-tokyo/monitoring.scm`](https://git.tojo.tokyo/monitoring.git/tree/tojo-tokyo/monitoring.scm?id=9dadbd397af96b53b14050e741618657f6d7bf33)
ファイルのヘッダの部分を参照してください。



## パイプを開いて df の出力を読む

まずは Guile から `df` コマンドを呼び出してみましょう パイプを開くために
[`open-input-pipe`](https://www.gnu.org/software/guile/manual/html_node/Pipes.html)
手続きを使います。 `open-input-pipe` には `(ice-9 popen)` モジュールが必要で、ポートから文字を読み込むのに
[`(ice-9
textual-ports)`](https://www.gnu.org/software/guile/manual/html_node/Textual-I_002fO.html)
モジュールの手続きを利用しています。



``` scheme
(use-modules (ice-9 popen)
             (ice-9 textual-ports))

(let ((port (open-input-pipe "df")))
  (display (get-string-all port))
  (close-pipe port))
```


    Filesystem     1K-blocks     Used Available Use% Mounted on
    none              492120        0    492120   0% /dev
    /dev/vda3       24377696 10096084  13020252  44% /
    tmpfs             501932        0    501932   0% /dev/shm
    0




ヘッダ行が先頭に一つありそれより下の行に1個以上の空白で区切られた値として必要な情報が出力されるようです。ヘッダ行は無視することにしてそれ以下を
Guile の連想リストのリストにできればよさそうです。



## 文字列を空白で区切る手続きを作る

1個以上の空白で区切られた文字列をリストにする手続きを Guile を軽く探したのですが見つけられなかったので自分で作ります。正規表現を扱う
[`(ice-9
regex)`](https://www.gnu.org/software/guile/manual/html_node/Regexp-Functions.html)
を使って下記のように実装します。



``` scheme
(use-modules (ice-9 regex))

(define (split-with-spaces x)
  (map match:substring (list-matches "[^ ]+" x)))

(split-with-spaces " hello  world ")
```


    ("hello" "world")




空白以外の文字が一個以上連続したものを集めることで空白で区切られた文字列のリストを作っています。



## df 手続きを実装する

これから作成する `df` は無引数で呼びだして、`df` コマンドの出力のそれぞれの行を連想リストに変換したものを返す手続きです。

今回は `split-with-spaces` を使用して下記のように実装しました。
なお、本記事の目的はコマンドの出力をパイプで一行づつ処理をするという
Unix でよくある処理を Guile でやる方法を例示することなので意図的に手続き的に書いています。



``` scheme
(define (df)
  (let ((port (open-input-pipe "df")))
    (get-line port)                     ; ヘッダ行を読み飛ばす
    (let loop ((line (get-line port))
               (table '()))
      (cond ((eof-object? line)
             (close-pipe port)
             table)
            (else
             (loop (get-line port)
                   (cons (map cons
                              '(filesystem 1k-blocks used available use% mounted-on)
                              (split-with-spaces line))
                         table)))))))
```



`df`
コマンドの出力を一行ずつ読んで連想リストを作成していき、最後まで読みきったらパイプをクローズした後に構築した連想リストのリストを返却します。

`df`
手続きの結果を整形して出力してみましょう([`format`](https://www.gnu.org/software/guile/manual/html_node/Formatted-Output.html)
手続きのために `(ice-9 format)`,
[`match-lambda`](https://www.gnu.org/software/guile/manual/html_node/Pattern-Matching.html)
のために `(ice-9 match)` モジュールを使っています)。



``` scheme
(use-modules (ice-9 match)
             (ice-9 format))

(for-each (lambda (record) 
            (for-each (match-lambda 
                        ((key . val)
                         (format #t "~a: ~a~%"
                                 (symbol->string key)
                                 val)))
                      record)
            (newline))
          (df))
```


``` 
filesystem: tmpfs
1k-blocks: 501932
used: 0
available: 501932
use%: 0%
mounted-on: /dev/shm

filesystem: /dev/vda3
1k-blocks: 24377696
used: 10096084
available: 13020252
use%: 44%
mounted-on: /

filesystem: none
1k-blocks: 492120
used: 0
available: 492120
use%: 0%
mounted-on: /dev

```




意図したとおりに実装できているようです。



## ディスク使用率がしきい値を超えているかを調べる



`df` 手続きを使ってしきい値以上のディスク使用率になっているファイルシステムがあったら真を返す、`disk-use%-over?`
手続きを作ってみましょう。

私は `/dev/` から始まっているファイルシステムにしか興味ないので、`/dev/` から始まっている文字列かどうかを判定する述語
`prefix-/dev/?` を作成します。



``` scheme
(define (prefix-/dev/? x)
  (and (string? x)
       (<= 5 (string-length x))
       (string=? (substring x 0 5) "/dev/")))

(map prefix-/dev/? '("/tmp/shm" "/dev/hello" "/neko/dev"))
```


    (#f #t #f)




`use%` は `%` を含んだ文字列になってしまっていて数値比較をするには扱いにくい状態です。`x%`
のような形になっている文字列を数字に変換する手続きを作成します。



``` scheme
(define (use%->number x)
  (string->number (string-delete #\% x)))

(use%->number "42%")
```


``` 
42
```




それでは `prefix-/dev/?`、`use%->number` を利用して `disk-use%-over?` を実装してみましょう
(`(srfi srfi-26)` の [`cut`
構文](https://www.gnu.org/software/guile/manual/html_node/SRFI_002d26.html)を使っています。`(cut
f <> x)` は `(lambda (y) (f y x))` のように書くのと同じです)。



``` scheme
(use-modules (srfi srfi-26)
             ((srfi srfi-1) #:select (any)))

(define (disk-use%-over? threshold)
  (any (cut < threshold <>)
       (map (compose use%->number (cut assoc-ref <> 'use%))
            (filter (compose prefix-/dev/? (cut assoc-ref <> 'filesystem))
                    (df)))))

(map disk-use%-over? '(40 50))
```


    (#t #f)




しきい値が `40` の場合は真を返し、`50` の場合は偽を返しました。`/dev/vda3` の現在のディスク使用率は `44%`
なので意図した挙動をしているようです。



## おわりに

無事に Guile を使ってディスク使用率を調べる手続きを作成することができました。この Web サイトは Guix System
で動いていて現在上記のプログラムを使って実際に監視を実施しています([該当するコード](https://git.tojo.tokyo/guix-config.git/tree/web/config.scm?id=1562b9827baa6a2c78592bd5f9115ed6a8b444b3#n33)、Guix
System では
[G-Expressions](https://guix.gnu.org/manual/en/html_node/G_002dExpressions.html)
という仕組みを利用して定期実行ジョブを Guile で気軽に実装できます)。このように GNU Guile では Unix
のコマンドと連携する実用的なプログラムを簡単に書くことができるのでみなさんも是非 GNU Guile
を使ってみてください。