6.3 ネットワークマップ
インフラストラクチャの物理的・論理的構造を視覚化するZabbixネットワークマップの包括的活用法
概要
ネットワークマップは、IT インフラストラクチャの構成要素とその相互関係を視覚的に表現し、システム全体の状況を直感的に把握できる重要な機能です。適切に設計されたマップにより、複雑なインフラの状態監視、障害の影響範囲把握、そして迅速な問題解決が可能になります。
ネットワークマップの価値
要素 | 効果 | 適用場面 |
---|---|---|
トポロジ可視化 | 構成要素の関係性把握 | システム理解・設計確認 |
リアルタイム状態 | 即座の状況把握 | 障害対応・運用監視 |
影響範囲分析 | 障害の波及効果確認 | 緊急時対応・リスク評価 |
ナビゲーション | 効率的な詳細アクセス | 日常運用・調査作業 |
コミュニケーション | 関係者間での共通理解 | 会議・報告・教育 |
マップの作成と編集
基本的なマップ作成
マップ要素の配置
yaml
# 基本マップ構成
マップ名: "Data Center Overview"
サイズ: 1200x800
背景: "白色"
グリッド: "有効(20pxスナップ)"
# 基本要素
要素タイプ:
ホスト:
表示: "アイコン + 名前"
状態連動: "有効"
位置: "論理的配置"
ホストグループ:
表示: "グループアイコン"
状態: "最悪状態表示"
階層: "部門別・機能別"
トリガー:
表示: "アラート状況"
色分け: "深刻度別"
集約: "同種問題まとめ"
画像:
背景図: "データセンター見取り図"
アイコン: "機器種別表示"
ロゴ: "企業・部門識別"
実践的なマップレイアウト
yaml
# データセンターマップ設計例
レイヤー構成:
Layer 1 - インフラ基盤:
- ネットワーク機器(ルーター・スイッチ)
- 電源設備(UPS・配電盤)
- 空調設備
Layer 2 - サーバー層:
- 物理サーバー
- 仮想化ホスト
- ストレージシステム
Layer 3 - アプリケーション層:
- Webサーバー
- アプリケーションサーバー
- データベースサーバー
Layer 4 - サービス層:
- 業務システム
- 外部連携
- ユーザーアクセス
# 地理的配置例
物理配置:
東京データセンター:
座標: (100, 100)
アイコン: "建物"
サイズ: "大"
大阪バックアップサイト:
座標: (600, 300)
アイコン: "建物"
サイズ: "中"
クラウド(AWS東京):
座標: (100, 500)
アイコン: "クラウド"
サイズ: "大"
クラウド(AWS大阪):
座標: (600, 600)
アイコン: "クラウド"
サイズ: "中"
アイコンとリンクの設定
カスタムアイコンライブラリ
yaml
# アイコンカテゴリ分類
サーバーアイコン:
物理サーバー:
- "server_physical.png"
- "server_rack.png"
- "server_blade.png"
仮想サーバー:
- "server_virtual.png"
- "vm_instance.png"
- "container.png"
ネットワークアイコン:
コアネットワーク:
- "router_core.png"
- "switch_l3.png"
- "firewall.png"
アクセスネットワーク:
- "switch_access.png"
- "wireless_ap.png"
- "load_balancer.png"
アプリケーションアイコン:
Webシステム:
- "web_server.png"
- "app_server.png"
- "database.png"
監視システム:
- "monitoring.png"
- "log_server.png"
- "backup.png"
クラウドアイコン:
プロバイダー別:
- "aws_cloud.png"
- "azure_cloud.png"
- "gcp_cloud.png"
サービス別:
- "cloud_compute.png"
- "cloud_storage.png"
- "cloud_network.png"
動的リンク設定
yaml
# リンクタイプと表現
リンク種別:
物理接続:
線種: "実線"
色: "黒"
太さ: "2px"
用途: "物理ケーブル接続"
論理接続:
線種: "破線"
色: "青"
太さ: "1px"
用途: "VLAN・VPN"
依存関係:
線種: "点線"
色: "グレー"
太さ: "1px"
用途: "サービス依存"
データフロー:
線種: "矢印付き実線"
色: "緑"
太さ: "2px"
用途: "データ流れ"
# 状態連動リンク
状態表現:
正常:
色: "緑"
太さ: "通常"
透明度: "100%"
警告:
色: "オレンジ"
太さ: "太い"
透明度: "100%"
点滅: "有効"
障害:
色: "赤"
太さ: "最太"
透明度: "100%"
点滅: "高速"
未監視:
色: "グレー"
太さ: "細い"
透明度: "50%"
動的マップ要素
リアルタイム状態反映
状態色分けシステム
yaml
# 包括的状態色定義
状態カラーパレット:
正常系:
- 正常: "#4CAF50" (緑)
- 情報: "#2196F3" (青)
- メンテナンス: "#9C27B0" (紫)
注意系:
- 警告: "#FF9800" (オレンジ)
- 軽微: "#FFC107" (黄)
問題系:
- 重要: "#FF5722" (オレンジ赤)
- 重大: "#F44336" (赤)
- 災害: "#B71C1C" (濃赤)
その他:
- 未監視: "#9E9E9E" (グレー)
- 無効: "#E0E0E0" (淡グレー)
- 不明: "#795548" (茶)
# アニメーション効果
動的表現:
点滅:
警告: "1秒間隔"
重大: "0.5秒間隔"
災害: "0.3秒間隔"
フェード:
メンテナンス: "2秒フェードイン/アウト"
復旧: "正常色への3秒グラデーション"
拡大縮小:
新規問題: "1.2倍に拡大後通常サイズ"
解決: "0.8倍に縮小後通常サイズ"
自動レイアウト機能
動的配置アルゴリズム
python
#!/usr/bin/env python3
"""Zabbix ネットワークマップ自動レイアウト"""
import math
import json
import requests
from typing import Dict, List, Tuple, Any
class NetworkMapAutoLayout:
def __init__(self, zabbix_api):
self.api = zabbix_api
self.layout_algorithms = {
'hierarchical': self.hierarchical_layout,
'circular': self.circular_layout,
'force_directed': self.force_directed_layout,
'geographical': self.geographical_layout
}
def hierarchical_layout(self, nodes: List[Dict], links: List[Dict],
width: int = 1200, height: int = 800) -> Dict[str, Tuple[int, int]]:
"""階層型レイアウト(トップダウン)"""
# 階層レベル計算
levels = {}
root_nodes = []
# ルートノード特定(依存関係なし)
all_targets = set(link['target'] for link in links)
for node in nodes:
if node['id'] not in all_targets:
root_nodes.append(node['id'])
levels[node['id']] = 0
# BFS で階層レベル設定
queue = root_nodes[:]
while queue:
current = queue.pop(0)
current_level = levels[current]
# 子ノード処理
for link in links:
if link['source'] == current and link['target'] not in levels:
levels[link['target']] = current_level + 1
queue.append(link['target'])
# 各レベルのノード配置
level_groups = {}
for node_id, level in levels.items():
if level not in level_groups:
level_groups[level] = []
level_groups[level].append(node_id)
positions = {}
max_level = max(level_groups.keys()) if level_groups else 0
level_height = height / (max_level + 1) if max_level > 0 else height
for level, node_ids in level_groups.items():
y = level_height * (level + 0.5)
node_width = width / (len(node_ids) + 1)
for i, node_id in enumerate(node_ids):
x = node_width * (i + 1)
positions[node_id] = (int(x), int(y))
return positions
def circular_layout(self, nodes: List[Dict], links: List[Dict],
width: int = 1200, height: int = 800) -> Dict[str, Tuple[int, int]]:
"""円形レイアウト"""
center_x, center_y = width // 2, height // 2
radius = min(width, height) // 3
positions = {}
node_count = len(nodes)
for i, node in enumerate(nodes):
angle = 2 * math.pi * i / node_count
x = center_x + radius * math.cos(angle)
y = center_y + radius * math.sin(angle)
positions[node['id']] = (int(x), int(y))
return positions
def force_directed_layout(self, nodes: List[Dict], links: List[Dict],
width: int = 1200, height: int = 800,
iterations: int = 100) -> Dict[str, Tuple[int, int]]:
"""力学的レイアウト(spring-force)"""
# 初期位置(ランダム)
import random
positions = {}
for node in nodes:
positions[node['id']] = (
random.randint(50, width - 50),
random.randint(50, height - 50)
)
# 力学シミュレーション
k = math.sqrt(width * height / len(nodes)) # 理想的な距離
for iteration in range(iterations):
forces = {node['id']: [0, 0] for node in nodes}
# 反発力(すべてのノード間)
for i, node1 in enumerate(nodes):
for j, node2 in enumerate(nodes):
if i != j:
pos1 = positions[node1['id']]
pos2 = positions[node2['id']]
dx = pos1[0] - pos2[0]
dy = pos1[1] - pos2[1]
distance = math.sqrt(dx*dx + dy*dy) or 1
# クーロン力
force = k * k / distance
fx = force * dx / distance
fy = force * dy / distance
forces[node1['id']][0] += fx
forces[node1['id']][1] += fy
# 引力(リンクで接続されたノード間)
for link in links:
source_pos = positions[link['source']]
target_pos = positions[link['target']]
dx = target_pos[0] - source_pos[0]
dy = target_pos[1] - source_pos[1]
distance = math.sqrt(dx*dx + dy*dy) or 1
# フック力
force = distance * distance / k
fx = force * dx / distance
fy = force * dy / distance
forces[link['source']][0] += fx
forces[link['source']][1] += fy
forces[link['target']][0] -= fx
forces[link['target']][1] -= fy
# 位置更新
temperature = max(0.1, 1.0 - iteration / iterations)
for node in nodes:
node_id = node['id']
fx, fy = forces[node_id]
# 移動量制限
displacement = math.sqrt(fx*fx + fy*fy) or 1
max_displacement = temperature * min(width, height) / 10
if displacement > max_displacement:
fx = fx / displacement * max_displacement
fy = fy / displacement * max_displacement
# 新しい位置
new_x = max(50, min(width - 50, positions[node_id][0] + fx))
new_y = max(50, min(height - 50, positions[node_id][1] + fy))
positions[node_id] = (int(new_x), int(new_y))
return positions
def geographical_layout(self, nodes: List[Dict], links: List[Dict],
width: int = 1200, height: int = 800) -> Dict[str, Tuple[int, int]]:
"""地理的レイアウト(GPS座標ベース)"""
# ノードから地理情報取得
geo_data = {}
for node in nodes:
# インベントリから地理情報取得(例)
inventory = self.get_host_inventory(node['id'])
if 'location_lat' in inventory and 'location_lon' in inventory:
try:
lat = float(inventory['location_lat'])
lon = float(inventory['location_lon'])
geo_data[node['id']] = (lat, lon)
except ValueError:
continue
if not geo_data:
# 地理情報がない場合は force_directed に fallback
return self.force_directed_layout(nodes, links, width, height)
# 緯度経度の範囲取得
lats = [coord[0] for coord in geo_data.values()]
lons = [coord[1] for coord in geo_data.values()]
min_lat, max_lat = min(lats), max(lats)
min_lon, max_lon = min(lons), max(lons)
# 画面座標に変換
positions = {}
margin = 50
for node_id, (lat, lon) in geo_data.items():
if max_lat != min_lat:
y = margin + (height - 2*margin) * (max_lat - lat) / (max_lat - min_lat)
else:
y = height // 2
if max_lon != min_lon:
x = margin + (width - 2*margin) * (lon - min_lon) / (max_lon - min_lon)
else:
x = width // 2
positions[node_id] = (int(x), int(y))
# 地理情報がないノードは近隣に配置
positioned_nodes = set(geo_data.keys())
for node in nodes:
if node['id'] not in positioned_nodes:
# 接続されたノードの近くに配置
connected_positions = []
for link in links:
if link['source'] == node['id'] and link['target'] in positioned_nodes:
connected_positions.append(positions[link['target']])
elif link['target'] == node['id'] and link['source'] in positioned_nodes:
connected_positions.append(positions[link['source']])
if connected_positions:
# 接続ノードの中央付近に配置
avg_x = sum(pos[0] for pos in connected_positions) / len(connected_positions)
avg_y = sum(pos[1] for pos in connected_positions) / len(connected_positions)
positions[node['id']] = (int(avg_x), int(avg_y))
else:
# デフォルト位置
positions[node['id']] = (width // 2, height // 2)
return positions
def get_host_inventory(self, host_id: str) -> Dict[str, str]:
"""ホストインベントリ情報取得"""
try:
inventory = self.api.host.get(
hostids=[host_id],
selectInventory=True,
output=['inventory']
)
return inventory[0].get('inventory', {}) if inventory else {}
except:
return {}
def apply_layout_to_map(self, map_id: str, layout_type: str = 'hierarchical') -> bool:
"""マップに自動レイアウト適用"""
# 現在のマップ取得
map_data = self.api.map.get(
sysmapids=[map_id],
selectSelements="extend",
selectLinks="extend"
)[0]
# ノードとリンクデータ準備
nodes = []
links = []
for element in map_data['selements']:
nodes.append({
'id': element['selementid'],
'type': element['elementtype'],
'resource_id': element.get('elementid', '')
})
for link in map_data['links']:
links.append({
'source': link['selementid1'],
'target': link['selementid2']
})
# レイアウト計算
if layout_type in self.layout_algorithms:
positions = self.layout_algorithms[layout_type](
nodes, links,
int(map_data['width']),
int(map_data['height'])
)
else:
raise ValueError(f"Unknown layout type: {layout_type}")
# マップ要素位置更新
updated_elements = []
for element in map_data['selements']:
if element['selementid'] in positions:
x, y = positions[element['selementid']]
element['x'] = str(x)
element['y'] = str(y)
updated_elements.append(element)
# マップ更新
update_result = self.api.map.update(
sysmapid=map_id,
selements=updated_elements
)
return 'sysmapids' in update_result
# 使用例
def main():
from zabbix_api import ZabbixAPI
# Zabbix API接続
zapi = ZabbixAPI("https://zabbix.example.com")
zapi.login("admin", "password")
layout_manager = NetworkMapAutoLayout(zapi)
# 自動レイアウト適用
map_id = "123"
success = layout_manager.apply_layout_to_map(map_id, 'hierarchical')
if success:
print(f"Map {map_id} layout updated successfully")
else:
print(f"Failed to update map {map_id} layout")
if __name__ == "__main__":
main()
マップ階層とナビゲーション
階層構造設計
多層マップアーキテクチャ
yaml
# マップ階層設計
階層レベル:
Level 0 - 全体概要:
マップ名: "Global Infrastructure Overview"
要素:
- 地域別データセンター
- クラウドプロバイダー
- 主要ネットワーク接続
- 全体SLA状況
Level 1 - 地域別:
マップ名: "Japan Region"
要素:
- 東京データセンター
- 大阪データセンター
- 地域間接続
- 地域サービス状況
Level 2 - データセンター別:
マップ名: "Tokyo Data Center"
要素:
- ネットワークセグメント
- サーバーラック
- インフラ設備
- 環境監視
Level 3 - セグメント別:
マップ名: "DMZ Network"
要素:
- 個別サーバー
- ネットワーク機器
- セキュリティ機器
- 詳細状態監視
# ナビゲーション設定
ナビゲーション要素:
ドリルダウンリンク:
- クリックで下位階層へ
- ダブルクリックで詳細ビュー
- 右クリックでコンテキストメニュー
ブレッドクラム:
- 現在位置表示
- 上位階層への直接移動
- 階層構造の明示
ミニマップ:
- 全体構造の俯瞰
- 現在位置のハイライト
- 直接位置移動
動的ナビゲーション
インテリジェントなマップ遷移
javascript
// 動的マップナビゲーションクラス
class ZabbixMapNavigator {
constructor(mapContainerId, apiEndpoint) {
this.container = document.getElementById(mapContainerId);
this.apiEndpoint = apiEndpoint;
this.currentMap = null;
this.mapHistory = [];
this.mapCache = new Map();
this.initNavigationUI();
this.setupEventHandlers();
}
initNavigationUI() {
// ナビゲーションツールバー作成
const toolbar = document.createElement('div');
toolbar.className = 'map-navigation-toolbar';
toolbar.innerHTML = `
<button id="nav-back" disabled>← 戻る</button>
<div id="breadcrumb"></div>
<button id="nav-home">🏠 ホーム</button>
<select id="map-selector">
<option value="">マップ選択...</option>
</select>
<button id="auto-layout">🔄 自動配置</button>
<button id="refresh">↻ 更新</button>
`;
this.container.parentNode.insertBefore(toolbar, this.container);
// ミニマップ作成
const minimap = document.createElement('div');
minimap.className = 'minimap';
minimap.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
width: 200px;
height: 150px;
border: 1px solid #ccc;
background: rgba(255,255,255,0.9);
z-index: 1000;
`;
this.container.appendChild(minimap);
this.minimap = minimap;
}
setupEventHandlers() {
// 戻るボタン
document.getElementById('nav-back').addEventListener('click', () => {
this.navigateBack();
});
// ホームボタン
document.getElementById('nav-home').addEventListener('click', () => {
this.navigateToHome();
});
// マップ選択
document.getElementById('map-selector').addEventListener('change', (e) => {
if (e.target.value) {
this.navigateToMap(e.target.value);
}
});
// 自動レイアウト
document.getElementById('auto-layout').addEventListener('click', () => {
this.applyAutoLayout();
});
// 更新
document.getElementById('refresh').addEventListener('click', () => {
this.refreshCurrentMap();
});
// マップ要素クリック
this.container.addEventListener('click', (e) => {
this.handleMapElementClick(e);
});
// マップ要素ダブルクリック
this.container.addEventListener('dblclick', (e) => {
this.handleMapElementDoubleClick(e);
});
// 右クリックコンテキストメニュー
this.container.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showContextMenu(e);
});
}
async navigateToMap(mapId, addToHistory = true) {
try {
// キャッシュチェック
let mapData = this.mapCache.get(mapId);
if (!mapData) {
// API からマップデータ取得
const response = await fetch(`${this.apiEndpoint}/maps/${mapId}`);
mapData = await response.json();
this.mapCache.set(mapId, mapData);
}
// 履歴管理
if (addToHistory && this.currentMap) {
this.mapHistory.push(this.currentMap);
document.getElementById('nav-back').disabled = false;
}
this.currentMap = mapId;
this.renderMap(mapData);
this.updateBreadcrumb();
this.updateMinimap();
} catch (error) {
console.error('マップナビゲーションエラー:', error);
this.showError('マップの読み込みに失敗しました');
}
}
navigateBack() {
if (this.mapHistory.length > 0) {
const previousMap = this.mapHistory.pop();
this.navigateToMap(previousMap, false);
if (this.mapHistory.length === 0) {
document.getElementById('nav-back').disabled = true;
}
}
}
navigateToHome() {
this.mapHistory = [];
this.navigateToMap('home-map', false);
document.getElementById('nav-back').disabled = true;
}
handleMapElementClick(event) {
const element = event.target.closest('.map-element');
if (!element) return;
const elementId = element.dataset.elementId;
const elementType = element.dataset.elementType;
// 要素選択ハイライト
this.highlightElement(element);
// 詳細情報パネル表示
this.showElementDetails(elementId, elementType);
}
handleMapElementDoubleClick(event) {
const element = event.target.closest('.map-element');
if (!element) return;
const elementId = element.dataset.elementId;
const elementType = element.dataset.elementType;
const subMapId = element.dataset.subMapId;
if (subMapId) {
// サブマップに遷移
this.navigateToMap(subMapId);
} else if (elementType === 'host') {
// ホスト詳細ページへ
this.openHostDetails(elementId);
} else if (elementType === 'hostgroup') {
// ホストグループ専用マップ生成
this.generateHostGroupMap(elementId);
}
}
showContextMenu(event) {
const element = event.target.closest('.map-element');
if (!element) return;
const elementId = element.dataset.elementId;
const elementType = element.dataset.elementType;
const contextMenu = document.createElement('div');
contextMenu.className = 'context-menu';
contextMenu.style.cssText = `
position: absolute;
left: ${event.pageX}px;
top: ${event.pageY}px;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.2);
z-index: 2000;
`;
const menuItems = this.getContextMenuItems(elementType, elementId);
contextMenu.innerHTML = menuItems.map(item =>
`<div class="menu-item" data-action="${item.action}">${item.label}</div>`
).join('');
document.body.appendChild(contextMenu);
// メニュー項目クリック処理
contextMenu.addEventListener('click', (e) => {
const action = e.target.dataset.action;
this.handleContextMenuAction(action, elementId, elementType);
document.body.removeChild(contextMenu);
});
// 外部クリックでメニュー閉じる
setTimeout(() => {
document.addEventListener('click', () => {
if (document.body.contains(contextMenu)) {
document.body.removeChild(contextMenu);
}
}, { once: true });
}, 0);
}
getContextMenuItems(elementType, elementId) {
const commonItems = [
{ action: 'details', label: '📊 詳細情報' },
{ action: 'history', label: '📈 履歴グラフ' },
{ action: 'events', label: '📋 イベント履歴' }
];
const typeSpecificItems = {
'host': [
{ action: 'dashboard', label: '🎛️ ダッシュボード' },
{ action: 'inventory', label: '📝 インベントリ' },
{ action: 'maintenance', label: '🔧 メンテナンス設定' }
],
'hostgroup': [
{ action: 'group_map', label: '🗺️ グループマップ生成' },
{ action: 'group_dashboard', label: '📊 グループダッシュボード' }
],
'trigger': [
{ action: 'acknowledge', label: '✅ 確認' },
{ action: 'suppress', label: '🔇 抑制' }
]
};
return [...commonItems, ...(typeSpecificItems[elementType] || [])];
}
handleContextMenuAction(action, elementId, elementType) {
switch (action) {
case 'details':
this.showElementDetails(elementId, elementType);
break;
case 'history':
this.openHistoryGraphs(elementId, elementType);
break;
case 'dashboard':
this.openHostDashboard(elementId);
break;
case 'group_map':
this.generateHostGroupMap(elementId);
break;
case 'maintenance':
this.openMaintenanceDialog(elementId);
break;
// 他のアクション...
}
}
async generateHostGroupMap(hostGroupId) {
try {
// ホストグループの動的マップ生成
const response = await fetch(`${this.apiEndpoint}/maps/generate-hostgroup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hostGroupId })
});
const mapData = await response.json();
// 一時マップとして表示
this.renderMap(mapData, true);
} catch (error) {
console.error('ホストグループマップ生成エラー:', error);
}
}
renderMap(mapData, isTemporary = false) {
// マップレンダリング実装
this.container.innerHTML = `
<div class="map-canvas" style="position: relative; width: 100%; height: 600px; border: 1px solid #ccc;">
${mapData.elements.map(element => this.renderMapElement(element)).join('')}
${mapData.links.map(link => this.renderMapLink(link)).join('')}
</div>
`;
if (!isTemporary) {
// 定期更新開始
this.startAutoRefresh();
}
}
renderMapElement(element) {
const statusClass = this.getStatusClass(element.status);
const iconUrl = this.getElementIcon(element.type, element.subtype);
return `
<div class="map-element ${statusClass}"
data-element-id="${element.id}"
data-element-type="${element.type}"
data-sub-map-id="${element.subMapId || ''}"
style="position: absolute; left: ${element.x}px; top: ${element.y}px;">
<img src="${iconUrl}" alt="${element.name}" width="32" height="32">
<div class="element-label">${element.name}</div>
</div>
`;
}
updateMinimap() {
// ミニマップ更新
const minimapCanvas = document.createElement('canvas');
minimapCanvas.width = 200;
minimapCanvas.height = 150;
const ctx = minimapCanvas.getContext('2d');
// 簡易版マップ描画
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, 200, 150);
// 要素の概略位置表示
const elements = this.container.querySelectorAll('.map-element');
elements.forEach(element => {
const rect = element.getBoundingClientRect();
const containerRect = this.container.getBoundingClientRect();
const x = (rect.left - containerRect.left) / containerRect.width * 200;
const y = (rect.top - containerRect.top) / containerRect.height * 150;
ctx.fillStyle = this.getElementColor(element);
ctx.fillRect(x - 1, y - 1, 3, 3);
});
this.minimap.innerHTML = '';
this.minimap.appendChild(minimapCanvas);
}
startAutoRefresh() {
// 30秒ごとに状態更新
this.refreshInterval = setInterval(() => {
this.refreshElementStates();
}, 30000);
}
async refreshElementStates() {
try {
const response = await fetch(`${this.apiEndpoint}/maps/${this.currentMap}/status`);
const statusData = await response.json();
// 要素状態更新
statusData.elements.forEach(elementStatus => {
const element = this.container.querySelector(`[data-element-id="${elementStatus.id}"]`);
if (element) {
element.className = element.className.replace(/status-\w+/, `status-${elementStatus.status}`);
}
});
this.updateMinimap();
} catch (error) {
console.error('状態更新エラー:', error);
}
}
}
// 使用例
const mapNavigator = new ZabbixMapNavigator('map-container', '/api/zabbix');
mapNavigator.navigateToMap('home-map');
地理的マップ
GIS統合
地理情報システム連携
javascript
// 地理的マップ統合クラス
class ZabbixGeoMap {
constructor(containerId, mapConfig) {
this.container = document.getElementById(containerId);
this.config = mapConfig;
this.markers = new Map();
this.clusters = new Map();
this.initMap();
this.loadZabbixData();
}
initMap() {
// Leaflet.js を使用した地図初期化
this.map = L.map(this.container.id).setView(
[this.config.defaultLat || 35.6762, this.config.defaultLng || 139.6503], // 東京
this.config.defaultZoom || 6
);
// 地図タイル設定
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(this.map);
// カスタムアイコン定義
this.icons = {
datacenter: L.divIcon({
className: 'datacenter-icon',
html: '<div class="icon-datacenter">🏢</div>',
iconSize: [40, 40]
}),
server: L.divIcon({
className: 'server-icon',
html: '<div class="icon-server">🖥️</div>',
iconSize: [30, 30]
}),
network: L.divIcon({
className: 'network-icon',
html: '<div class="icon-network">🌐</div>',
iconSize: [25, 25]
}),
cloud: L.divIcon({
className: 'cloud-icon',
html: '<div class="icon-cloud">☁️</div>',
iconSize: [35, 35]
})
};
// クラスター設定
this.markerClusterGroup = L.markerClusterGroup({
maxClusterRadius: 50,
spiderfyOnMaxZoom: true,
showCoverageOnHover: false,
iconCreateFunction: (cluster) => {
const count = cluster.getChildCount();
const statusCounts = this.getClusterStatusCounts(cluster);
return L.divIcon({
html: `<div class="cluster-icon cluster-${statusCounts.worst}">
<span>${count}</span>
</div>`,
className: 'marker-cluster',
iconSize: [40, 40]
});
}
});
this.map.addLayer(this.markerClusterGroup);
}
async loadZabbixData() {
try {
// Zabbix からホスト情報取得
const response = await fetch('/api/zabbix/hosts-with-geo');
const hosts = await response.json();
// マーカー作成
hosts.forEach(host => {
if (host.inventory.location_lat && host.inventory.location_lon) {
this.createHostMarker(host);
}
});
// ネットワーク接続線描画
this.drawNetworkConnections(hosts);
// 定期更新開始
this.startStatusUpdates();
} catch (error) {
console.error('Zabbixデータ読み込みエラー:', error);
}
}
createHostMarker(host) {
const lat = parseFloat(host.inventory.location_lat);
const lng = parseFloat(host.inventory.location_lon);
if (isNaN(lat) || isNaN(lng)) return;
// ホストタイプに基づくアイコン選択
const iconType = this.getHostIconType(host);
const icon = this.icons[iconType];
// マーカー作成
const marker = L.marker([lat, lng], { icon })
.bindPopup(this.createHostPopup(host))
.on('click', () => this.onHostMarkerClick(host));
// 状態に基づく色設定
this.updateMarkerStatus(marker, host.status);
// クラスターグループに追加
this.markerClusterGroup.addLayer(marker);
// マーカー管理
this.markers.set(host.hostid, marker);
}
getHostIconType(host) {
const tags = host.tags || [];
const hostGroups = host.groups || [];
// タグ・グループ情報からアイコンタイプ判定
if (tags.some(tag => tag.tag === 'datacenter' || tag.tag === 'dc')) {
return 'datacenter';
} else if (hostGroups.some(group => group.name.includes('Cloud'))) {
return 'cloud';
} else if (hostGroups.some(group => group.name.includes('Network'))) {
return 'network';
} else {
return 'server';
}
}
createHostPopup(host) {
const latestData = host.latestData || {};
return `
<div class="host-popup">
<h3>${host.name}</h3>
<div class="host-info">
<div><strong>IP:</strong> ${host.interfaces[0]?.ip || 'N/A'}</div>
<div><strong>OS:</strong> ${host.inventory.os || 'N/A'}</div>
<div><strong>場所:</strong> ${host.inventory.location || 'N/A'}</div>
</div>
<div class="host-metrics">
<div>CPU: ${latestData.cpu || 'N/A'}%</div>
<div>Memory: ${latestData.memory || 'N/A'}%</div>
<div>Disk: ${latestData.disk || 'N/A'}%</div>
</div>
<div class="host-actions">
<button onclick="openHostDashboard('${host.hostid}')">ダッシュボード</button>
<button onclick="openHostGraphs('${host.hostid}')">グラフ</button>
</div>
</div>
`;
}
drawNetworkConnections(hosts) {
// ネットワーク接続の可視化
const connections = this.analyzeNetworkConnections(hosts);
connections.forEach(connection => {
const source = hosts.find(h => h.hostid === connection.source);
const target = hosts.find(h => h.hostid === connection.target);
if (source?.inventory.location_lat && target?.inventory.location_lat) {
const sourceLat = parseFloat(source.inventory.location_lat);
const sourceLng = parseFloat(source.inventory.location_lon);
const targetLat = parseFloat(target.inventory.location_lat);
const targetLng = parseFloat(target.inventory.location_lon);
const polyline = L.polyline([
[sourceLat, sourceLng],
[targetLat, targetLng]
], {
color: this.getConnectionColor(connection.type),
weight: this.getConnectionWeight(connection.bandwidth),
opacity: 0.7
}).addTo(this.map);
polyline.bindTooltip(`${connection.type}: ${connection.bandwidth || 'Unknown'}`);
}
});
}
analyzeNetworkConnections(hosts) {
// ネットワーク接続分析(簡略化版)
const connections = [];
// 同一サブネット内の接続を推定
const subnets = {};
hosts.forEach(host => {
const ip = host.interfaces[0]?.ip;
if (ip) {
const subnet = ip.split('.').slice(0, 3).join('.');
if (!subnets[subnet]) subnets[subnet] = [];
subnets[subnet].push(host.hostid);
}
});
// サブネット内接続生成
Object.values(subnets).forEach(hostIds => {
if (hostIds.length > 1) {
for (let i = 0; i < hostIds.length - 1; i++) {
connections.push({
source: hostIds[i],
target: hostIds[i + 1],
type: 'LAN',
bandwidth: '1Gbps'
});
}
}
});
return connections;
}
updateMarkerStatus(marker, status) {
const element = marker.getElement();
if (element) {
element.className = element.className.replace(/status-\w+/, `status-${status}`);
}
}
startStatusUpdates() {
// 60秒ごとに状態更新
setInterval(async () => {
try {
const response = await fetch('/api/zabbix/hosts-status');
const statusUpdates = await response.json();
statusUpdates.forEach(update => {
const marker = this.markers.get(update.hostid);
if (marker) {
this.updateMarkerStatus(marker, update.status);
}
});
} catch (error) {
console.error('状態更新エラー:', error);
}
}, 60000);
}
// フィルタリング機能
filterByStatus(status) {
this.markers.forEach((marker, hostId) => {
const host = this.hosts.find(h => h.hostid === hostId);
if (status === 'all' || host.status === status) {
this.markerClusterGroup.addLayer(marker);
} else {
this.markerClusterGroup.removeLayer(marker);
}
});
}
filterByHostGroup(groupName) {
this.markers.forEach((marker, hostId) => {
const host = this.hosts.find(h => h.hostid === hostId);
const hasGroup = host.groups.some(g => g.name === groupName);
if (groupName === 'all' || hasGroup) {
this.markerClusterGroup.addLayer(marker);
} else {
this.markerClusterGroup.removeLayer(marker);
}
});
}
// ヒートマップ表示
showMetricHeatmap(metric) {
const heatData = [];
this.markers.forEach((marker, hostId) => {
const host = this.hosts.find(h => h.hostid === hostId);
const lat = parseFloat(host.inventory.location_lat);
const lng = parseFloat(host.inventory.location_lon);
const value = host.latestData[metric] || 0;
if (!isNaN(lat) && !isNaN(lng)) {
heatData.push([lat, lng, value / 100]);
}
});
// ヒートマップ描画(HeatLayer.js使用)
if (this.heatLayer) {
this.map.removeLayer(this.heatLayer);
}
this.heatLayer = L.heatLayer(heatData, {
radius: 25,
blur: 15,
maxZoom: 17,
gradient: {
0.0: 'blue',
0.5: 'yellow',
1.0: 'red'
}
}).addTo(this.map);
}
}
// 使用例
const geoMapConfig = {
defaultLat: 35.6762,
defaultLng: 139.6503,
defaultZoom: 6
};
const zabbixGeoMap = new ZabbixGeoMap('geo-map-container', geoMapConfig);
マップの自動化
API活用
プログラマティックマップ管理
python
#!/usr/bin/env python3
"""Zabbix マップ自動化スクリプト"""
import json
import requests
from typing import Dict, List, Any, Optional
from datetime import datetime
class ZabbixMapAutomation:
def __init__(self, zabbix_url: str, auth_token: str):
self.zabbix_url = zabbix_url
self.auth_token = auth_token
self.session = requests.Session()
def create_infrastructure_map(self, name: str, hostgroups: List[str],
layout_type: str = 'hierarchical') -> str:
"""インフラストラクチャマップ自動生成"""
# ホストグループ情報取得
hostgroup_data = self._get_hostgroups(hostgroups)
# マップ要素準備
elements = []
links = []
element_id_counter = 1
# ホストグループ要素追加
group_positions = self._calculate_group_positions(hostgroup_data, layout_type)
for group in hostgroup_data:
elements.append({
"selementid": str(element_id_counter),
"elementtype": "4", # ホストグループ
"elementid": group["groupid"],
"label": group["name"],
"x": str(group_positions[group["groupid"]][0]),
"y": str(group_positions[group["groupid"]][1]),
"iconid_off": "152", # デフォルトホストグループアイコン
"iconid_on": "152"
})
element_id_counter += 1
# 関連リンク分析・追加
links = self._analyze_group_relationships(hostgroup_data)
# マップ作成
map_data = {
"jsonrpc": "2.0",
"method": "map.create",
"params": {
"name": name,
"width": 1200,
"height": 800,
"backgroundid": 0,
"label_type": 0,
"label_location": 0,
"highlight": 1,
"expandproblem": 1,
"markelements": 1,
"show_unack": 1,
"selements": elements,
"links": links
},
"auth": self.auth_token,
"id": 1
}
response = self.session.post(self.zabbix_url, json=map_data)
result = response.json()
if "result" in result:
return result["result"]["sysmapids"][0]
else:
raise Exception(f"Map creation failed: {result}")
def create_geographic_map(self, name: str, hosts_with_geo: List[Dict[str, Any]]) -> str:
"""地理的マップ自動生成"""
# 地理座標の正規化
latitudes = [float(h['lat']) for h in hosts_with_geo if h.get('lat')]
longitudes = [float(h['lng']) for h in hosts_with_geo if h.get('lng')]
if not latitudes or not longitudes:
raise ValueError("地理座標情報が不足しています")
min_lat, max_lat = min(latitudes), max(latitudes)
min_lng, max_lng = min(longitudes), max(longitudes)
# マップサイズに正規化
map_width, map_height = 1200, 800
margin = 50
elements = []
element_id_counter = 1
for host in hosts_with_geo:
if host.get('lat') and host.get('lng'):
# 地理座標をマップ座標に変換
if max_lat != min_lat:
y = margin + (map_height - 2*margin) * (max_lat - float(host['lat'])) / (max_lat - min_lat)
else:
y = map_height // 2
if max_lng != min_lng:
x = margin + (map_width - 2*margin) * (float(host['lng']) - min_lng) / (max_lng - min_lng)
else:
x = map_width // 2
elements.append({
"selementid": str(element_id_counter),
"elementtype": "0", # ホスト
"elementid": host["hostid"],
"label": host["name"],
"x": str(int(x)),
"y": str(int(y)),
"iconid_off": self._get_host_icon_id(host),
"iconid_on": self._get_host_icon_id(host)
})
element_id_counter += 1
# 地理的マップ作成
map_data = {
"jsonrpc": "2.0",
"method": "map.create",
"params": {
"name": name,
"width": map_width,
"height": map_height,
"backgroundid": 0,
"label_type": 2, # ラベル表示
"label_location": 3, # 下部
"highlight": 1,
"expandproblem": 1,
"markelements": 1,
"show_unack": 1,
"selements": elements
},
"auth": self.auth_token,
"id": 1
}
response = self.session.post(self.zabbix_url, json=map_data)
result = response.json()
if "result" in result:
return result["result"]["sysmapids"][0]
else:
raise Exception(f"Geographic map creation failed: {result}")
def create_service_dependency_map(self, service_name: str) -> str:
"""サービス依存関係マップ生成"""
# サービス関連ホスト取得
service_hosts = self._get_service_hosts(service_name)
# 依存関係分析
dependencies = self._analyze_service_dependencies(service_hosts)
# 階層レイアウト計算
layers = self._calculate_dependency_layers(dependencies)
elements = []
links = []
element_id_counter = 1
# レイヤー別ホスト配置
layer_height = 800 / (len(layers) + 1)
for layer_idx, layer_hosts in enumerate(layers):
y = layer_height * (layer_idx + 1)
host_width = 1200 / (len(layer_hosts) + 1)
for host_idx, host in enumerate(layer_hosts):
x = host_width * (host_idx + 1)
elements.append({
"selementid": str(element_id_counter),
"elementtype": "0",
"elementid": host["hostid"],
"label": host["name"],
"x": str(int(x)),
"y": str(int(y)),
"iconid_off": self._get_service_icon_id(host),
"iconid_on": self._get_service_icon_id(host)
})
host["selementid"] = str(element_id_counter)
element_id_counter += 1
# 依存関係リンク作成
for dep in dependencies:
source_element = next((h for h in service_hosts if h["hostid"] == dep["source"]), None)
target_element = next((h for h in service_hosts if h["hostid"] == dep["target"]), None)
if source_element and target_element:
links.append({
"selementid1": source_element["selementid"],
"selementid2": target_element["selementid"],
"drawtype": 0, # 線
"color": "00AA00" # 緑
})
# サービス依存関係マップ作成
map_data = {
"jsonrpc": "2.0",
"method": "map.create",
"params": {
"name": f"Service Dependencies - {service_name}",
"width": 1200,
"height": 800,
"backgroundid": 0,
"label_type": 2,
"label_location": 3,
"highlight": 1,
"expandproblem": 1,
"markelements": 1,
"show_unack": 1,
"selements": elements,
"links": links
},
"auth": self.auth_token,
"id": 1
}
response = self.session.post(self.zabbix_url, json=map_data)
result = response.json()
if "result" in result:
return result["result"]["sysmapids"][0]
else:
raise Exception(f"Service dependency map creation failed: {result}")
def update_map_with_monitoring_data(self, map_id: str) -> bool:
"""監視データに基づくマップ更新"""
# 現在のマップ取得
map_data = self._get_map(map_id)
# 各要素の最新状態取得
updated_elements = []
for element in map_data["selements"]:
if element["elementtype"] == "0": # ホスト
host_status = self._get_host_status(element["elementid"])
# アイコン更新
if host_status["status"] == "0": # 監視中
if host_status["has_problems"]:
element["iconid_off"] = "154" # 問題ありアイコン
element["iconid_on"] = "154"
else:
element["iconid_off"] = "151" # 正常アイコン
element["iconid_on"] = "151"
else: # 監視停止
element["iconid_off"] = "156" # 無効アイコン
element["iconid_on"] = "156"
updated_elements.append(element)
# マップ更新
update_data = {
"jsonrpc": "2.0",
"method": "map.update",
"params": {
"sysmapid": map_id,
"selements": updated_elements
},
"auth": self.auth_token,
"id": 1
}
response = self.session.post(self.zabbix_url, json=update_data)
result = response.json()
return "result" in result
def _get_hostgroups(self, group_names: List[str]) -> List[Dict[str, Any]]:
"""ホストグループ情報取得"""
data = {
"jsonrpc": "2.0",
"method": "hostgroup.get",
"params": {
"filter": {"name": group_names},
"selectHosts": "count",
"output": ["groupid", "name"]
},
"auth": self.auth_token,
"id": 1
}
response = self.session.post(self.zabbix_url, json=data)
return response.json()["result"]
def _calculate_group_positions(self, groups: List[Dict[str, Any]],
layout_type: str) -> Dict[str, tuple]:
"""グループ位置計算"""
positions = {}
if layout_type == "hierarchical":
# 階層レイアウト
rows = len(groups)
row_height = 800 / (rows + 1)
for i, group in enumerate(groups):
x = 600 # 中央
y = row_height * (i + 1)
positions[group["groupid"]] = (int(x), int(y))
elif layout_type == "circular":
# 円形レイアウト
center_x, center_y = 600, 400
radius = 300
for i, group in enumerate(groups):
angle = 2 * 3.14159 * i / len(groups)
x = center_x + radius * math.cos(angle)
y = center_y + radius * math.sin(angle)
positions[group["groupid"]] = (int(x), int(y))
return positions
def _analyze_group_relationships(self, groups: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""グループ間関係分析"""
# 簡略化:隣接グループ間にリンク生成
links = []
for i in range(len(groups) - 1):
links.append({
"selementid1": str(i + 1),
"selementid2": str(i + 2),
"drawtype": 0,
"color": "0000AA"
})
return links
# 使用例
def main():
automation = ZabbixMapAutomation(
zabbix_url="https://zabbix.example.com/api_jsonrpc.php",
auth_token="auth_token_here"
)
# インフラマップ自動生成
infra_map_id = automation.create_infrastructure_map(
name="Auto-Generated Infrastructure Map",
hostgroups=["Linux servers", "Windows servers", "Network devices"],
layout_type="hierarchical"
)
print(f"Created infrastructure map: {infra_map_id}")
# 地理的マップ生成
hosts_with_geo = [
{"hostid": "10001", "name": "Tokyo-Server-01", "lat": "35.6762", "lng": "139.6503"},
{"hostid": "10002", "name": "Osaka-Server-01", "lat": "34.6937", "lng": "135.5023"},
{"hostid": "10003", "name": "Nagoya-Server-01", "lat": "35.1815", "lng": "136.9066"}
]
geo_map_id = automation.create_geographic_map(
name="Japan Geographic Map",
hosts_with_geo=hosts_with_geo
)
print(f"Created geographic map: {geo_map_id}")
if __name__ == "__main__":
main()
ベストプラクティス
設計原則
効果的なマップ設計
yaml
# マップ設計ガイドライン
レイアウト原則:
情報階層:
- 重要度による配置順序
- 論理的なグルーピング
- 明確な視覚的流れ
- 適切な余白確保
色の使い方:
- 状態による一貫した色分け
- 色覚障害への配慮
- 十分なコントラスト
- 企業カラーとの調和
アイコン選択:
- 直感的で理解しやすい
- 統一されたデザインスタイル
- 適切なサイズとスケール
- 状態変化の明確な表現
# パフォーマンス考慮事項
最適化原則:
要素数制限:
- 1マップあたり50要素以下
- 必要に応じて階層化
- 動的フィルタリング活用
更新頻度:
- リアルタイム要求: 30秒
- 一般監視: 1-5分
- 統計情報: 15-60分
ネットワーク効率:
- 差分更新の実装
- キャッシュ戦略
- 圧縮とCDN活用
運用上の考慮事項
マップライフサイクル管理
yaml
# マップ管理戦略
維持管理:
定期レビュー:
- 月次レイアウト見直し
- 四半期機能追加
- 年次全体再設計
変更管理:
- インフラ変更の反映
- 組織改編への対応
- 新サービス追加
品質保証:
- 表示速度測定
- ユーザビリティテスト
- 情報精度確認
ドキュメント化:
- マップ仕様書
- 操作マニュアル
- トラブルシューティング手順
# アクセシビリティ
利用しやすさ:
視覚的配慮:
- 色覚障害対応
- 高コントラストモード
- 文字サイズ調整
操作性:
- キーボードナビゲーション
- タッチデバイス対応
- スクリーンリーダー互換
多言語対応:
- UI要素の翻訳
- 右から左への言語
- フォント選択
まとめ
効果的なネットワークマップは、IT インフラストラクチャの可視化と運用効率化の重要な基盤です。
重要ポイント
- 階層化設計: 概要から詳細への段階的情報表示
- リアルタイム反映: 監視データと連動した動的状態表示
- 地理的統合: 物理的配置を反映した直感的な構造
- 自動化活用: API とスクリプトによる効率的なマップ管理
次のステップ
次章では、レポート機能について詳しく解説し、監視データの定期的な集計・分析・配信手法を学習します。