【Python】Paramikoを使ってssh接続でサーバーのリソース監視をやってみよう
この記事の目次
はじめに
今回はPythonのSSH接続ライブラリであるParamikoを使ってサーバーの簡易的な死活監視プログラムを作ってみます。監視する項目としては
- RAM使用率
- ストレージ容量
について監視を行うプログラムをPythonで作成してみます。
環境構築
Dockerを用いてSSH serverとSSH clientのコンテナを作成して開発を行います。
VMや外部のレンタルサーバーなどの選択肢もありますが、ローカルでサクッとテストしたいのと、Dockerのネットワーク周りの勉強がてらこの方法でいきます。
アーキテクチャ
各コンテナにIPアドレスを付与するためにDockerのブリッジネットワークという機能を使います。この機能により、Dockerホスト内で独立したネットワークを構築でき、同じブリッジネットワークに接続されたコンテナ間で通信が可能になります。各コンテナにはプライベートなIPアドレスが割り当てられます。
今回の設定では、サブネット10.10.0.0/24を持つssh_explorer_test_networkというブリッジネットワークに以下の二つのコンテナを接続します。
- ssh-serverコンテナ
- IPアドレス、10.10.0.5を付与。
- 死活監視対象のサーバーを模したコンテナ。ユーザー名はserver、パスワードはpasswordでssh接続が可能。
- 図には書いてませんが、22番ポートをブリッジネットワーク間でのみ開放しています
- ssh-clientコンテナ
- IPアドレス、10.10.0.6を付与。
- 死活監視プログラムを実行するためのコンテナ。
docker-compose.ymlとその他
インフラの説明がそのまま反映されているだけなので、ここはサラッといきます。
ディレクトリ構造
ssh_explorer ├── docker-compose.yml ├── ssh-client │ ├── Dockerfile │ ├── requirements.txt │ └── src │ ├── main.py │ ├── get_ram_status.sh │ └── get_strage_status.sh └── ssh-server └── Dockerfile
docker-compose.yml
version: '3.8' services: ssh-server: image: ssh-server build: ./ssh-server tty: true networks: ssh_explorer_test_network: ipv4_address: 10.10.0.5 ssh-client: image: ssh-client build: ./ssh-client tty: true volumes: - ./ssh-client/src:/root/src networks: ssh_explorer_test_network: ipv4_address: 10.10.0.6 networks: ssh_explorer_test_network: driver: bridge ipam: config: - subnet: 10.10.0.0/24
ssh-clientのDockerfile
# 基本イメージ FROM python:3.11.9-bookworm RUN apt-get update && \ apt-get upgrade -y &&\ apt-get install -y openssh-client python3-pip iputils-ping WORKDIR /root COPY ./requirements.txt /root RUN pip install --upgrade pip RUN pip install -r requirements.txt
sshおよび、paramikoを実行するためのパッケージがインストールされる構造になっています。iputils-ping はpingを実行するためのパッケージです。デフォルトのベースイメージにはインストールされていなかったので、別途インストールしています。
requirements.txtはparamikoしか記述されていないので省略。
ssh-serverのDockerfile
# 基本イメージ FROM ubuntu:22.04 # 必要なパッケージのインストール RUN apt-get update && apt-get install -y openssh-server sudo # SSH用のユーザーを追加 RUN useradd -rm -d /home/ubuntu -s /bin/bash -g root -G sudo -u 1000 server RUN echo 'server:password' | chpasswd # SSH接続用の設定 RUN mkdir /var/run/sshd RUN sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config # パスワード認証を有効にする RUN sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config # SSH接続のためのポートを開放 EXPOSE 22 # SSHサーバーを起動 CMD ["/usr/sbin/sshd", "-D"]
ssh-serverをインストールし、指定のユーザー名とパスワード(server:password) を自動で設定できる仕組みになっています。
ChatGPTの出力を見て、「sedってそうやって使うのか、、、、」と勉強になりました。
これにてコンテナのセットアップは完了です。
sshおよび監視を行うためのPythonコード
ここからpythonコードを用いてsshを行い、レスポンスを受け取るコードを書きます。
受け取る出力はシェルスクリプトを一工夫して、JSON形式で出力できるようにします。
ソースコードのディレクトリ構造
root └ src ├── main.py ├── get_ram_status.sh └── get_strage_status.sh
main.py
import paramiko import ast from pprint import pprint all_shells_info = [ {"value_name": "ram_status","file_name": "get_ram_status.sh"}, {"value_name": "strage_status","file_name": "get_strage_status.sh"}, ] def join_ssh_commands(all_shells_info: list): command = "echo '['" for shell in all_shells_info: value_name = shell["value_name"] file_name = shell["file_name"] command += \ "&& echo " + \ "\"{'name': " + "'" +value_name+ "'" + \ "," + " 'value': \" " + \ "&& " with open(file_name, "r", encoding='utf-8') as file: for line in file: if (not line[0] == "#") and (not line == ""): command += line command += "&& echo \"},\" " command += "&& echo ']'" return command.replace("\n","") def ssh_command(hostname, port, username, password, command): client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(hostname, port=port, username=username, password=password) # コマンドの実行 stdin, stdout, stderr = client.exec_command(command) # 出力結果の取得 output = stdout.read().decode('utf-8') error = stderr.read().decode('utf-8') # 接続のクローズ client.close() return output, error def main(): command = join_ssh_commands(all_shells_info) output_str, error_str = ssh_command("10.10.0.5", 22, "server" ,"password", command) output = ast.literal_eval(output_str) pprint(output) if __name__ == "__main__": main()
get_ram_status.sh
#!/bin/bash free -m | awk 'NR==2 {print "{\"total(MB)\": " $2 ", \"used(MB)\": " $3 ", \"free(MB)\": " $4 ", \"shared(MB)\": " $5 ", \"buff/cache(MB)\": " $6 ", \"available(MB)\": " $7 "}"}'
get_strage_status.sh
#!/bin/bash echo "[" && df -BM | awk 'NR>1 {print "{\"Filesystem\": \"" $1 "\", \"Size(MB)\": " substr($2, 1, length($2)-1) ", \"Used(MB)\": " substr($3, 1, length($3)-1) ", \"Available(MB)\": " substr($4, 1, length($4)-1) ", \"Use(%)\": " substr($5, 1, length($5)-1) ", \"MountedOn\": \"" $6 "\"},"}' | sed ':a;N;$!ba;s/\\n/, /g' && echo "]"
解説
上記のmain.pyを実行すると以下の結果が得られます。
json形式で取得できるのがこのコードの推しポイントです。
[ { 'name': 'ram_status', 'value': { 'available(MB)': 2262, 'buff/cache(MB)': 2333, 'free(MB)': 277, 'shared(MB)': 113, 'total(MB)': 3933, 'used(MB)': 1322 } },{ 'name': 'strage_status', 'value': [ { 'Available(MB)': 31482, 'Filesystem': 'overlay', 'MountedOn': '/', 'Size(MB)': 59768, 'Use(%)': 45, 'Used(MB)': 25219 },{ 'Available(MB)': 64, 'Filesystem': 'tmpfs', 'MountedOn': '/dev', 'Size(MB)': 64, 'Use(%)': 0, 'Used(MB)': 0 } ] } ]
main.pyでssh接続とサーバー上でのデータの収集をおこないます。
基本的な動作は以下の流れになっております。
1:シェルスクリプト( get_ram_status.sh、get_strage_status.sh)を読み込む。
2:シェルスクリプトを&&で連結して、1度の接続で同時に実行できるコマンドとして読み込む
3:連結したコマンドを実行する&出力を得る
ここから詳しく解説していきます。
まずライブラリを読み込みます。
import paramiko import ast from pprint import pprint
今回はsshにはparamiko、取得した文字列をPythonの値として読み込むライブラリとしてastを使います。
ssh時に実行するシェルスクリプトを辞書型で定義します。
ここに記載されているシェルスクリプトを連結して実行します。
all_shells_info = [ {"value_name": "ram_status","file_name": "get_ram_status.sh"}, {"value_name": "strage_status","file_name": "get_strage_status.sh"}, ]
シェルスクリプトを連結する関数です。
echoで[]を出力して、pythonのリストになるように出力を整形しています。
また&&と改行を消す処理を使って、最終的にシェルスクリプトがワンライナーになるように整形します。
def join_ssh_commands(all_shells_info: list): command = "echo '['" for shell in all_shells_info: value_name = shell["value_name"] file_name = shell["file_name"] command += \ "&& echo " + \ "\"{'name': " + "'" +value_name+ "'" + \ "," + " 'value': \" " + \ "&& " with open(file_name, "r", encoding='utf-8') as file: for line in file: if (not line[0] == "#") and (not line == ""): command += line command += "&& echo \"},\" " command += "&& echo ']'" return command.replace("\n","")
実際にssh接続を行いコマンドを実行する関数です。
注意点としてはparamiko.AutoAddPolicy()の部分です。ここでは接続先のホストが未知のホストであっても自動的に接続を許可する設定を行なっています。
def ssh_command(hostname, port, username, password, command): client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(hostname, port=port, username=username, password=password) # コマンドの実行 stdin, stdout, stderr = client.exec_command(command) # 出力結果の取得 output = stdout.read().decode('utf-8') error = stderr.read().decode('utf-8') # 接続のクローズ client.close() return output, error
最後にmain.pyで全ての関数を実行します。繰り返しにはなりますが、以下の流れを体現できるような流れになっています。
1:シェルスクリプト( get_ram_status.sh、get_strage_status.sh)を読み込む。
2:シェルスクリプトを&&で連結して、1度の接続で同時に実行できるコマンドとして読み込む
3:連結したコマンドを実行する&出力を得る
def main(): command = join_ssh_commands(all_shells_info) output_str, error_str = ssh_command("10.10.0.5", 22, "server" ,"password", command) output = ast.literal_eval(output_str) pprint(output) if __name__ == "__main__": main()
まとめ
今回は簡単ですが、sshを使った簡単なリソース監視をしてみました。
今ではあまりない状況かもしれませんが、どうしてもrestAPIを立てられない監視側に、何も追加せずに監視だけしたいケースなどで役にたつ知識だと思います。
それでは良いエンジニアライフを!!!
カテゴリー: