pysnmpの話
SNMP周りをちゃんと()整理し,pysnmpというライブラリについて使ってみました.この記事書き始めたのは2023の年末だったけどpysnmpの実験が間に合ってなくて…
さらにTerraformしてたから尚更…
軽くこんなライブラリ使ってみたよって話にしようと思ったらかなり長くなってしまいました.
不備がありそうなので修正予定です.
SNMPとは
TCP/IPネットワーク上でネットワーク管理するためのプロトコルでUDP上で動作するらしいです.
Simple Network Management Protocolを略してSNMP.
登場人物は,SNMPマネージャーとSNMPエージェント.
3種類の通信
・get-request関連
いわゆるポーリングにあたると思われます.
1.マネージャーから参照要求
2.エージェントから応答
3.マネージャーから次の参照要求
4.エージェントから応答
…
・trap
エージェント側からマネージャーに通知する通信
・set-request関連
マネージャーからエージェントに設定を要求し,エージェントが設定変更.その後エージェントから正常終了か応答する.
1.マネージャから設定要求
2.エージェントから応答
トラップだけポート162番で通信するらしいです.他は161番.
その他用語
・MIB
SNMPで使われる情報=エージェントから取得する情報,エージェントに設定する情報を定義しているらしいです.
データ構造の表記法ASN.1を利用した管理情報構造であるSMIで記述されているらしく.
・SNMPv1
最初のバージョンで,コミュニティ名で認証している.暗号化されていない.
・SNMPv2c
コミュニティ名などは暗号化されていない.SNMPインフォームに対応.
SNMPインフォームってのはトラップみたいなものだけど,マネージャー側から応答があるらしいです.
・SNMPv3
ついに暗号化.コミュニティ名ではなく,ユーザ名による認証機能が導入されているらしく,ハッシュ関数を利用したデータ改ざん検知もできるらしいです.
・コミュニティ名
参考書とか漁ってるとさらっと出てくるコミュニティ名…そのくせ試験の解答に出てくるから全く…
それはさておき.
エージェントが複数ある時とかに,それらをグループ化するためのもの.
こちらがわかりやすいです.
ほかにも用語はありそうだけど,調べ切れないので割愛
pysnmp
SNMPをPythonで使ってみます.
別にPythonじゃなくても良いんですが,色々とあり.
あまりにも情報が少ない(特に日本語)なライブラリです.
準備
マネージャーとエージェントを用意します.
AWSのEC2インスタンス2台使いました.
Amazon linuxです.
実験段階なのでちゃんと設定してないです.
rootに上がってから,snmpのインストール
yum update
yum install -y net-snmp
yum install -y net-snmp-utils
起動してactiveになってることを確認.
systemctl start snmpd
systemctl status snmpd
エージェント側だけ以下を設定
vi /etc/snmp/snmpd.conf
ググった結果,エージェント側は以下だけコメントアウトを外しました.
com2sec notConfigUser default public
group notConfigGroup v2c notConfigUser
あと以下部分も変更しました.
#変更前
view systemview included .1.3.6.1.2.1.25.1.1
#変更後
view systemview included .1.3.6.1.2.1.25
変更前だと,Host Resources MIB,ホストの情報の一部しか取れないみたいです。
再起動.reloadでも良いかも.
systemctl restart snmpd
念の為firewallも入れておきました.
yum update
yum install firewalld
systemctl start firewalld.service
firewallに161のルールも追加しました.
よしやろうということでマネージャーからsnmpwalkを実行
snmpwalk -v 2c -c public 172.~
ノーレスポンス…
AWSのセキュリティグループでインバウンドルールに161を追加.
…ダメでした.
原因は単純で,firewalld,セキュリティグループともに161をTCPで指定してました.
UDPにしたところ問題なくsnmpwalkの結果を得られました.
色々時間かかりましたが,ここまでが前座.
python関連のインストールです.
pip install pysnmp
使用してみる
from pysnmp.hlapi import *
iterator = getCmd(
SnmpEngine(),
CommunityData('public',mpModel=1),
UdpTransportTarget(('172.~', 161)),
ContextData(),
ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0)),
ObjectType(ObjectIdentity('1.3.6.1.2.1.1.6.0')),
ObjectType(ObjectIdentity('HOST-RESOURCES-MIB', 'hrSWRunPerfCPU', 1)),
ObjectType(ObjectIdentity('IF-MIB', 'ifInOctets', 1))
)
errorIndication, errorStatus, errorIndex, varBinds = next(iterator)
if errorIndication:
print(errorIndication)
elif errorStatus:
print('%s at %s' % (errorStatus.prettyPrint(),
errorIndex and varBinds[int(errorIndex) - 1][0] or '?'))
else:
for varBind in varBinds:
print(' = '.join([x.prettyPrint() for x in varBind]))
これはpysnmpのドキュメントにあるコードのコピペです.OIDのところだけ変えてます.
上記コードの出力値の一部が以下です.
IF-MIB::ifInOctets.1 = No Such Object currently exists at this OID
HOST-RESOURCES-MIB::hrSWRunPerfCPU.1 = 121
ちゃんと取れないオブジェクトには取れないと出てますね.
ではもうちょっと詳しくコードの中身を見ていきます.
ライブラリのインポート部分は飛ばして…
iterator = getCmd(
SnmpEngine(),
CommunityData('public',mpModel=1),
UdpTransportTarget(('172.~', 161)),
ContextData(),
ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0)),
ObjectType(ObjectIdentity('1.3.6.1.2.1.1.6.0')),
ObjectType(ObjectIdentity('HOST-RESOURCES-MIB', 'hrSWRunPerfCPU', 1)),
ObjectType(ObjectIdentity('IF-MIB', 'ifInOctets', 1))
)
CommunityDataの第1引数ではコミュニティ名,mpModel=0でSNMPのバージョンが1,mpModel=1でバージョンが2らしいです.
UdpTransportTargetの引数は(ipアドレス,ポート番号)です.ホスト名でもいけるっぽいです.
ObjectType(ObjectIdentity(〜ではOIDを指定します.
errorIndication, errorStatus, errorIndex, varBinds = next(iterator)
ここはイテレータですので,pysnmpのgetCmd関数でsnmpwalkの結果を一つずつ取り出している感じらしいです.
if errorIndication:
print(errorIndication)
elif errorStatus:
print('%s at %s' % (errorStatus.prettyPrint(),
errorIndex and varBinds[int(errorIndex) - 1][0] or '?'))
else:
for varBind in varBinds:
print(' = '.join([x.prettyPrint() for x in varBind]))
この辺はエラーが起きた際の処理です.
Python触り続けてもう5年以上経つのに,if文でNoneかどうかで分岐するって初めて知りました.paizaとかatcoderやってた時に学んだのに忘れていただけかも…
最初の分岐でエラーがあってNoneじゃなかったらerrorIndicationが表示されるっぽいですね.
これはsnmpengineのエラーらしいです.
エージェント側のsnmpdを止めた状態でスクリプトを実行したところ,こちらの分岐に吸い込まれ以下のエラーが出ました.
No SNMP response received before timeout
この表示からファイアウォールとかで閉め出されていても出ると思われます.そこまで確認しておらずですが.
2個目の分岐ではerrorStatusが0だったら入り込まないけど,それ以外だったら入り込む.
エージェント側のエラーらしいです.この分岐への持ち込み方がいまいちわからず.
githubを読み込めばわかるかもですが,気力があったらで…
最後というか,それ以外の分岐がvarBindsの情報を表示するものです.
prettyPrint()っていうのはASN.1ベースのオブジェクトを読みやすい形に変換してくれるらしいです.英語力が終わってるので間違ってるかもですが…
.joinは学生時代に競プロで苦し紛れの出力でよく使っていたのですが,結合ですね,
以下例だと,配列の中身を1 2 3 4とstringで表示したい時に使うやつです.
a = [1,2,3,4]
print(' '.join(map(str,a)))
もっとシンプルな例は以下ですかね.
a = "www"
print('o'.join(a))
草からwowowが出力されます.
pysnmpの場合" = "で結合になると思われたのですが.が!
それ以前に,以下でもちゃんと上記出力が見れました.
print(varBinds[0])
ただ,エージェント側のsnmpdが止まっている,最初のエラーの分岐に行く時はout of rangeエラーが表示されたので,上記コードでは分岐で制御されているとはいえインデックス指定の書き方をしない方がいいかもですね.
あと上記リンクのprettyPrint()のQA部分で16進数から見やすくなると記載がありますし,無難に開発者の人たちのやり方に倣うのが良いと思われます.
ここまで色々インストールしたりするの面倒
これを見てpysnmp使ってみたいと思う人,遊びでやる分には準備が面倒くさいですよね.
ローカルに対して実行でもいいですけど,せっかくのsnmpですし…
そんな面倒くさがり屋の人のために,この遊びをTerraformで出来るようにしました.
pysnmpのコード作成とかpysnmp周りのテストまではできてないですが,マネージャーの方にssh接続してsnmpwalkはできました.
# 変数定義
variable "aws_access_key" {}
variable "aws_secret_key" {}
variable "key-name" {}
# AWS Provider
provider "aws" {
region = "ap-northeast-1"
access_key = var.aws_access_key
secret_key = var.aws_secret_key
}
#VPC
resource "aws_vpc" "test-vpc" {
cidr_block = "172.16.0.0/24"
tags = {
Name = "test-vpc"
}
}
#パブリックサブネット
resource "aws_subnet" "test-public-subnet" {
vpc_id = aws_vpc.test-vpc.id
cidr_block = "172.16.0.0/28"
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = true
tags = {
Name = "test-public-subnet"
}
}
#プライベートサブネット
resource "aws_subnet" "test-private-subnet" {
vpc_id = aws_vpc.test-vpc.id
cidr_block = "172.16.0.16/28"
availability_zone = "ap-northeast-1a"
tags = {
Name = "test-private-subnet"
}
}
#インターネットゲートウェイ
resource "aws_internet_gateway" "test-gateway" {
vpc_id = aws_vpc.test-vpc.id
tags = {
Name = "test-gateway"
}
}
#ルートテーブル
resource "aws_route_table" "test-route-table1" {
vpc_id = aws_vpc.test-vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.test-gateway.id
}
tags = {
Name = "test-route-table1"
}
}
resource "aws_route_table" "test-route-table2" {
vpc_id = aws_vpc.test-vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.test-nat.id
}
tags = {
Name = "test-route-table2"
}
}
#NATゲートウェイ
resource "aws_nat_gateway" "test-nat" {
allocation_id = aws_eip.test-eip.id
subnet_id = aws_subnet.test-public-subnet.id
tags = {
Name = "test-nat"
}
depends_on = [aws_internet_gateway.test-gateway]
}
#ルートテーブルとサブネットの紐付け
resource "aws_route_table_association" "test-association1" {
subnet_id = aws_subnet.test-public-subnet.id
route_table_id = aws_route_table.test-route-table1.id
}
resource "aws_route_table_association" "test-association2" {
subnet_id = aws_subnet.test-private-subnet.id
route_table_id = aws_route_table.test-route-table2.id
}
#セキュリティグループ作成
resource "aws_security_group" "test-security-group1" {
name = "test-security-group1"
description = "TEST"
vpc_id = aws_vpc.test-vpc.id
ingress {
description = "SNMP"
from_port = 161
to_port = 161
protocol = "udp"
cidr_blocks = ["172.16.0.16/28"]
}
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "test-security-group1"
}
}
resource "aws_security_group" "test-security-group2" {
name = "test-security-group2"
description = "TEST"
vpc_id = aws_vpc.test-vpc.id
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "test-security-group2"
}
}
#ENI
resource "aws_network_interface" "test-nw-interface-manager" {
subnet_id = aws_subnet.test-private-subnet.id
private_ips = ["172.16.0.20"]
security_groups = [aws_security_group.test-security-group2.id]
}
resource "aws_network_interface" "test-nw-interface-agent" {
subnet_id = aws_subnet.test-public-subnet.id
private_ips = ["172.16.0.5"]
security_groups = [aws_security_group.test-security-group1.id]
}
#elastic ip
resource "aws_eip" "test-eip" {
domain = "vpc"
}
#EIC
resource "aws_ec2_instance_connect_endpoint" "manager-eic" {
subnet_id = aws_subnet.test-private-subnet.id
security_group_ids = [
aws_security_group.test-security-group2.id,
]
}
#EC2
resource "aws_instance" "test-instance-manager" {
ami = "ami-0310b105770df9334"
instance_type = "t2.micro"
availability_zone = "ap-northeast-1a"
key_name = var.key-name
network_interface {
device_index = 0
network_interface_id = aws_network_interface.test-nw-interface-manager.id
}
user_data = <<-EOF
#!/bin/bash
sudo yum update
sudo yum install -y net-snmp
sudo yum install -y net-snmp-utils
sudo yum install -y firewalld
sudo systemctl start firewalld.service
sudo systemctl start snmpd
sudo yum install -y pip
sudo pip install pysnmp
EOF
tags = {
Name = "test-instance-manager"
}
}
resource "aws_instance" "test-instance-agent" {
ami = "ami-0310b105770df9334"
instance_type = "t2.micro"
availability_zone = "ap-northeast-1a"
key_name = var.key-name
network_interface {
device_index = 0
network_interface_id = aws_network_interface.test-nw-interface-agent.id
}
user_data = <<-EOF
#!/bin/bash
sudo yum update
sudo yum install -y net-snmp
sudo yum install -y net-snmp-utils
sudo yum install -y firewalld
sudo systemctl start firewalld.service
sudo firewall-cmd --permanent --add-port=161/udp
sudo systemctl restart firewalld.service
sudo bash -c 'echo view systemview included .1.3.6.1.2.1.25 >> /etc/snmp/snmpd.conf'
sudo bash -c 'echo com2sec notConfigUser default public >> /etc/snmp/snmpd.conf'
sudo bash -c 'echo group notConfigGroup v2c notConfigUser >> /etc/snmp/snmpd.conf'
sudo systemctl start snmpd
EOF
tags = {
Name = "test-instance-agent"
}
}
Discussion