Zabbix API ile Otomatik Host Ekleme ve Yönetim

Yüz sunucuyu tek tek Zabbix arayüzünden eklemek… Bunu bir kez yapmak zorunda kaldıysanız ne demek istediğimi anlarsınız. Mouse’a tıklamaktan elin uyuşuyor, her seferinde aynı template’i seçiyorsunuz, aynı grup atamasını yapıyorsunuz. Sonra bir baktınız iki saat geçmiş, hâlâ 60 sunucu var önünüzde. İşte tam bu noktada Zabbix API devreye giriyor ve hayatı anlamlı kılıyor.

Zabbix, 1.8 sürümünden beri JSON-RPC tabanlı bir API sunuyor. Günümüzde bu API o kadar olgunlaştı ki, arayüzden yapabileceğiniz neredeyse her şeyi programatik olarak halledebilirsiniz. Host ekleme, template atama, trigger yönetimi, maintenance penceresi açma… hepsi API üzerinden mümkün. Bu yazıda gerçek dünyada işe yarayan senaryolar üzerinden Zabbix API kullanımını ele alacağız. Python kullanacağız çünkü sysadmin dünyasında en yaygın seçenek bu.

Zabbix API Temelleri

Zabbix API, HTTP üzerinden çalışan JSON-RPC 2.0 protokolünü kullanıyor. Her istek bir method ve params içeriyor, her yanıt da result veya error döndürüyor. Kimlik doğrulama için önce token alıyorsunuz, sonra bu token’ı tüm isteklerde kullanıyorsunuz.

Zabbix 5.4 ile birlikte API token sistemi geldi, yani kullanıcı adı/şifre yerine kalıcı token kullanabilirsiniz. Ama çoğu ortamda hâlâ kullanıcı/şifre kombinasyonu görüyorum. Her ikisini de ele alacağız.

Önce basit bir test yapalım, curl ile API’nin çalışıp çalışmadığını kontrol edelim:

curl -s -X POST http://zabbix.sirketiniz.com/zabbix/api_jsonrpc.php 
  -H "Content-Type: application/json" 
  -d '{
    "jsonrpc": "2.0",
    "method": "apiinfo.version",
    "params": [],
    "id": 1
  }' | python3 -m json.tool

Bu isteğin güzel tarafı authentication gerektirmemesi. Yanıt olarak Zabbix versiyonunuzu göreceksiniz. Eğer {"jsonrpc":"2.0","result":"6.4.0","id":1} gibi bir şey dönüyorsa, API erişilebilir demektir.

Python ile Zabbix API Sınıfı Oluşturma

Her seferinde raw HTTP isteği yazmak yerine, işleri düzenli tutmak için küçük bir wrapper sınıfı yazalım. Bunu bir kez yapıp her projede kullanalım:

import requests
import json
import logging
from typing import Optional, Any

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

class ZabbixAPI:
    def __init__(self, url: str, verify_ssl: bool = True):
        self.url = f"{url}/api_jsonrpc.php"
        self.session = requests.Session()
        self.session.verify = verify_ssl
        self.session.headers.update({"Content-Type": "application/json"})
        self.auth_token = None
        self._request_id = 1

    def login(self, username: str, password: str) -> bool:
        """Kullanici adi ve sifre ile giris yapar, token alir."""
        result = self._request("user.login", {
            "username": username,
            "password": password
        }, auth_required=False)
        
        if result:
            self.auth_token = result
            logger.info(f"Zabbix API baglantiси basarili: {self.url}")
            return True
        return False

    def set_token(self, token: str):
        """Onceden olusturulmus API token'i kullanir."""
        self.auth_token = token

    def logout(self):
        """Oturumu kapat, token'i gecersiz kildir."""
        if self.auth_token:
            self._request("user.logout", [])
            self.auth_token = None
            logger.info("Zabbix API oturumu kapatildi")

    def _request(self, method: str, params: Any, 
                 auth_required: bool = True) -> Optional[Any]:
        payload = {
            "jsonrpc": "2.0",
            "method": method,
            "params": params,
            "id": self._request_id
        }
        
        if auth_required and self.auth_token:
            payload["auth"] = self.auth_token
        
        self._request_id += 1
        
        try:
            response = self.session.post(self.url, json=payload, timeout=30)
            response.raise_for_status()
            data = response.json()
            
            if "error" in data:
                logger.error(f"API hatasi [{method}]: {data['error']['data']}")
                return None
            
            return data.get("result")
        
        except requests.exceptions.ConnectionError:
            logger.error(f"Zabbix sunucusuna baglanamadı: {self.url}")
            return None
        except requests.exceptions.Timeout:
            logger.error("API istegi zaman asimi")
            return None
        except json.JSONDecodeError:
            logger.error("API'den gecersiz JSON yaniti alindi")
            return None

Bu sınıfı zabbix_api.py adıyla kaydedin. Bundan sonraki tüm örneklerde bu sınıfı kullanacağız.

Host Grubu ve Template Bilgilerini Çekme

Host eklemeden önce grup ID ve template ID bilgilerine ihtiyacınız var. Zabbix API isimleri değil, ID’leri kabul ediyor. Şu küçük yardımcı script bunu halleder:

from zabbix_api import ZabbixAPI
import os

def get_zabbix_ids():
    zapi = ZabbixAPI("http://zabbix.sirketiniz.com/zabbix")
    
    if not zapi.login(
        os.environ.get("ZABBIX_USER", "Admin"),
        os.environ.get("ZABBIX_PASS", "")
    ):
        print("Giris basarisiz!")
        return

    # Host gruplarini listele
    print("n=== HOST GRUPLARI ===")
    groups = zapi._request("hostgroup.get", {
        "output": ["groupid", "name"],
        "sortfield": "name"
    })
    
    if groups:
        for g in groups:
            print(f"  [{g['groupid']}] {g['name']}")

    # Template'leri listele - linux template'lerini filtrele
    print("n=== LINUX TEMPLATE'LER ===")
    templates = zapi._request("template.get", {
        "output": ["templateid", "name"],
        "search": {"name": "Linux"},
        "searchByAny": True,
        "sortfield": "name"
    })
    
    if templates:
        for t in templates:
            print(f"  [{t['templateid']}] {t['name']}")
    
    zapi.logout()

if __name__ == "__main__":
    get_zabbix_ids()

Bu script’i çalıştırınca önünüze ID listesi dökülür. O listeyi bir yere not edin çünkü sonraki adımlarda kullanacaksınız. Gerçek ortamlarda bu değerleri bir config dosyasına veya environment variable’a yazmanızı öneririm.

Tek Host Ekleme

Temel host ekleme işlemini anlayalım, sonra bunu toplu işleme çevireceğiz:

from zabbix_api import ZabbixAPI
import os

def add_single_host(hostname: str, ip: str, 
                    group_ids: list, template_ids: list,
                    port: int = 10050) -> Optional[str]:
    """
    Tek bir host ekler.
    
    Donus degerleri:
    - Basarili ise host ID (string)
    - Basarisiz ise None
    """
    zapi = ZabbixAPI("http://zabbix.sirketiniz.com/zabbix")
    
    if not zapi.login(
        os.environ.get("ZABBIX_USER"),
        os.environ.get("ZABBIX_PASS")
    ):
        return None

    # Grup listesini API formatina donustur
    groups = [{"groupid": str(gid)} for gid in group_ids]
    templates = [{"templateid": str(tid)} for tid in template_ids]
    
    result = zapi._request("host.create", {
        "host": hostname,          # Teknik isim (DNS adi veya hostname)
        "name": hostname,          # Goruntu adi (Zabbix arayuzunde gorunen)
        "status": 0,               # 0: izleme aktif, 1: izleme pasif
        "groups": groups,
        "templates": templates,
        "interfaces": [{
            "type": 1,             # 1: Agent, 2: SNMP, 3: IPMI, 4: JMX
            "main": 1,             # Birincil interface
            "useip": 1,            # 1: IP kullan, 0: DNS kullan
            "ip": ip,
            "dns": "",
            "port": str(port)
        }],
        "inventory_mode": 0,       # 0: Manuel envanter, 1: Otomatik
        "description": f"Otomatik eklendi - {hostname}"
    })
    
    zapi.logout()
    
    if result and "hostids" in result:
        host_id = result["hostids"][0]
        print(f"Host eklendi: {hostname} (ID: {host_id})")
        return host_id
    
    return None

# Kullanim
if __name__ == "__main__":
    host_id = add_single_host(
        hostname="prod-web-01.sirket.com",
        ip="10.0.1.101",
        group_ids=[12, 25],        # Linux Servers + Production gruplari
        template_ids=[10001, 10047] # Template Linux by Zabbix agent + Template App Nginx
    )

CSV Dosyasından Toplu Host Ekleme

Gerçek dünya senaryosu: Yeni bir proje başladı, 50 sunucu üretilebilir, bunları Zabbix’e eklemeniz gerekiyor. Önce bir CSV hazırlayın:

# servers.csv ornegi
cat > /tmp/servers.csv << 'EOF'
hostname,ip,groups,templates,port,environment
prod-web-01,10.0.1.101,"Linux Servers,Web Servers","Template Linux by Zabbix agent,Template App Nginx",10050,production
prod-web-02,10.0.1.102,"Linux Servers,Web Servers","Template Linux by Zabbix agent,Template App Nginx",10050,production
prod-db-01,10.0.2.101,"Linux Servers,Database Servers","Template Linux by Zabbix agent,Template App MySQL",10050,production
staging-web-01,10.0.10.101,"Linux Servers,Staging","Template Linux by Zabbix agent",10050,staging
EOF

Şimdi bu CSV’yi okuyup toplu ekleme yapan script:

import csv
import time
from zabbix_api import ZabbixAPI
import os
import logging

logger = logging.getLogger(__name__)

class ZabbixBulkImporter:
    def __init__(self, zabbix_url: str):
        self.zapi = ZabbixAPI(zabbix_url)
        self._group_cache = {}
        self._template_cache = {}
        
        if not self.zapi.login(
            os.environ.get("ZABBIX_USER"),
            os.environ.get("ZABBIX_PASS")
        ):
            raise ConnectionError("Zabbix API'ye baglanamadı")
    
    def _get_group_id(self, group_name: str) -> Optional[str]:
        """Grup adi ile ID'sini bulur, cache'de yoksa API'ye sorar."""
        group_name = group_name.strip()
        
        if group_name in self._group_cache:
            return self._group_cache[group_name]
        
        result = self.zapi._request("hostgroup.get", {
            "output": ["groupid", "name"],
            "filter": {"name": group_name}
        })
        
        if result:
            group_id = result[0]["groupid"]
            self._group_cache[group_name] = group_id
            return group_id
        
        # Grup yoksa olustur
        logger.warning(f"Grup bulunamadı, olusturuluyor: {group_name}")
        create_result = self.zapi._request("hostgroup.create", {
            "name": group_name
        })
        
        if create_result:
            group_id = create_result["groupids"][0]
            self._group_cache[group_name] = group_id
            return group_id
        
        return None
    
    def _get_template_id(self, template_name: str) -> Optional[str]:
        """Template adi ile ID'sini bulur."""
        template_name = template_name.strip()
        
        if template_name in self._template_cache:
            return self._template_cache[template_name]
        
        result = self.zapi._request("template.get", {
            "output": ["templateid", "name"],
            "filter": {"name": template_name}
        })
        
        if result:
            template_id = result[0]["templateid"]
            self._template_cache[template_name] = template_id
            return template_id
        
        logger.error(f"Template bulunamadı: {template_name}")
        return None
    
    def host_exists(self, hostname: str) -> bool:
        """Host zaten varsa True donerа."""
        result = self.zapi._request("host.get", {
            "output": ["hostid"],
            "filter": {"host": hostname}
        })
        return bool(result)
    
    def import_from_csv(self, csv_file: str, dry_run: bool = False):
        """CSV dosyasindan toplu host ekleme."""
        stats = {"added": 0, "skipped": 0, "failed": 0}
        
        with open(csv_file, "r", encoding="utf-8") as f:
            reader = csv.DictReader(f)
            hosts = list(reader)
        
        logger.info(f"Toplam {len(hosts)} host isllenecek")
        
        for i, row in enumerate(hosts, 1):
            hostname = row["hostname"].strip()
            ip = row["ip"].strip()
            port = int(row.get("port", 10050))
            
            logger.info(f"[{i}/{len(hosts)}] Isleniyor: {hostname}")
            
            # Duplicate kontrol
            if self.host_exists(hostname):
                logger.warning(f"  ATLANDI: {hostname} zaten mevcut")
                stats["skipped"] += 1
                continue
            
            # Grup ID'lerini coz
            group_names = [g.strip() for g in row["groups"].split(",")]
            group_ids = []
            for gname in group_names:
                gid = self._get_group_id(gname)
                if gid:
                    group_ids.append({"groupid": gid})
            
            # Template ID'lerini coz
            template_names = [t.strip() for t in row["templates"].split(",")]
            template_ids = []
            for tname in template_names:
                if tname:
                    tid = self._get_template_id(tname)
                    if tid:
                        template_ids.append({"templateid": tid})
            
            if not group_ids:
                logger.error(f"  HATALI: {hostname} icin gecerli grup bulunamadı")
                stats["failed"] += 1
                continue
            
            if dry_run:
                logger.info(f"  DRY-RUN: {hostname} ({ip}) eklenecekti")
                stats["added"] += 1
                continue
            
            # Host'u ekle
            result = self.zapi._request("host.create", {
                "host": hostname,
                "name": hostname,
                "status": 0,
                "groups": group_ids,
                "templates": template_ids,
                "interfaces": [{
                    "type": 1,
                    "main": 1,
                    "useip": 1,
                    "ip": ip,
                    "dns": "",
                    "port": str(port)
                }],
                "tags": [
                    {"tag": "environment", "value": row.get("environment", "")},
                    {"tag": "source", "value": "auto-import"}
                ]
            })
            
            if result:
                logger.info(f"  EKLENDI: {hostname} (ID: {result['hostids'][0]})")
                stats["added"] += 1
            else:
                logger.error(f"  BASARISIZ: {hostname} eklenemedi")
                stats["failed"] += 1
            
            # API'yi fazla zorlamayalim
            time.sleep(0.1)
        
        logger.info(f"nSonuc: {stats['added']} eklendi, "
                   f"{stats['skipped']} atlandi, "
                   f"{stats['failed']} basarisiz")
        
        self.zapi.logout()
        return stats


if __name__ == "__main__":
    importer = ZabbixBulkImporter("http://zabbix.sirketiniz.com/zabbix")
    
    # Once dry-run yapın, emin olunca dry_run=False ile calistirin
    importer.import_from_csv("/tmp/servers.csv", dry_run=True)

dry_run parametresine dikkat edin. Bu opsiyonu her toplu işlemde kullanın. Önce neyin ekleneceğini görün, sonra gerçek import’u çalıştırın. Kaç kez “bir script çalıştırdım, ne olduğunu anlamak için saatler geçirdim” durumuna düştüm, dry_run alışkanlığı bunun önüne geçiyor.

Var Olan Host’ları Güncelleme ve Template Yönetimi

Yeni template çıktı, yüzlerce sunucuya uygulamanız gerekiyor. El ile mi yapacaksınız? Hayır:

def update_hosts_template(zapi: ZabbixAPI, 
                          group_name: str, 
                          add_template_name: str,
                          remove_template_name: str = None):
    """
    Belirli bir gruptaki tum hostlara template ekler,
    eski template varsa kaldirir.
    """
    # Grup ID'sini bul
    groups = zapi._request("hostgroup.get", {
        "output": ["groupid"],
        "filter": {"name": group_name}
    })
    
    if not groups:
        logger.error(f"Grup bulunamadı: {group_name}")
        return
    
    group_id = groups[0]["groupid"]
    
    # Yeni template ID'sini bul
    new_templates = zapi._request("template.get", {
        "output": ["templateid"],
        "filter": {"name": add_template_name}
    })
    
    if not new_templates:
        logger.error(f"Template bulunamadı: {add_template_name}")
        return
    
    new_template_id = new_templates[0]["templateid"]
    
    # Gruptaki tum hostlari al
    hosts = zapi._request("host.get", {
        "output": ["hostid", "host"],
        "groupids": group_id,
        "selectTemplates": ["templateid", "name"]
    })
    
    logger.info(f"{group_name} grubunda {len(hosts)} host bulundu")
    
    for host in hosts:
        host_id = host["hostid"]
        
        # Mevcut template listesi
        current_templates = [
            {"templateid": t["templateid"]} 
            for t in host.get("templates", [])
            if t["name"] != remove_template_name  # Eski template'i cikar
        ]
        
        # Yeni template'i ekle (duplicate olmadan)
        template_ids = [t["templateid"] for t in current_templates]
        if new_template_id not in template_ids:
            current_templates.append({"templateid": new_template_id})
        
        # Host'u guncelle
        result = zapi._request("host.update", {
            "hostid": host_id,
            "templates": current_templates
        })
        
        if result:
            logger.info(f"  Guncellendi: {host['host']}")
        else:
            logger.error(f"  Basarisiz: {host['host']}")
        
        time.sleep(0.05)

Maintenance Penceresi Otomasyonu

Periyodik bakım için maintenance penceresi açmak da API üzerinden yapılabilir. Bir deploy script’ine bunu eklemek hayat kurtarır, bakım sırasında sahte alarmlar gelmiyor:

from datetime import datetime, timedelta

def create_maintenance(zapi: ZabbixAPI, 
                       maintenance_name: str,
                       hostnames: list,
                       duration_minutes: int = 60,
                       description: str = "") -> Optional[str]:
    """
    Belirtilen hostlar icin maintenance penceresi olusturur.
    Donus: Maintenance ID veya None
    """
    # Host ID'lerini al
    host_ids = []
    for hostname in hostnames:
        result = zapi._request("host.get", {
            "output": ["hostid"],
            "filter": {"host": hostname}
        })
        if result:
            host_ids.append({"hostid": result[0]["hostid"]})
        else:
            logger.warning(f"Host bulunamadı: {hostname}")
    
    if not host_ids:
        logger.error("Hicbir host bulunamadı")
        return None
    
    now = datetime.now()
    end_time = now + timedelta(minutes=duration_minutes)
    
    result = zapi._request("maintenance.create", {
        "name": maintenance_name,
        "description": description or f"Otomatik bakım - {now.strftime('%Y-%m-%d %H:%M')}",
        "active_since": int(now.timestamp()),
        "active_till": int(end_time.timestamp()),
        "maintenance_type": 0,   # 0: Veri toplama var, 1: Veri toplama yok
        "hosts": host_ids,
        "timeperiods": [{
            "timeperiod_type": 0,  # Tek seferlik
            "start_date": int(now.timestamp()),
            "period": duration_minutes * 60
        }]
    })
    
    if result:
        maint_id = result["maintenanceids"][0]
        logger.info(
            f"Maintenance olusturuldu: {maintenance_name} "
            f"(ID: {maint_id}, Sure: {duration_minutes} dakika)"
        )
        return maint_id
    
    return None


# Deploy script entegrasyonu ornegi
def deploy_with_maintenance(hostnames: list, deploy_func):
    """Deploy oncesi maintenance ac, deploy sonrasi kapat."""
    zapi = ZabbixAPI("http://zabbix.sirketiniz.com/zabbix")
    zapi.login(os.environ["ZABBIX_USER"], os.environ["ZABBIX_PASS"])
    
    maint_id = create_maintenance(
        zapi,
        maintenance_name=f"Deploy - {datetime.now().strftime('%Y%m%d-%H%M')}",
        hostnames=hostnames,
        duration_minutes=30,
        description="Otomatik deploy maintenance"
    )
    
    try:
        deploy_func()
    finally:
        # Her durumda maintenance'i kapat
        if maint_id:
            zapi._request("maintenance.delete", [maint_id])
            logger.info("Maintenance kapatildi")
        zapi.logout()

Ansible ile Zabbix API Entegrasyonu

Eğer Ansible kullanıyorsanız, yeni sunucu provision ettiğinizde otomatik olarak Zabbix’e eklemek çok mantıklı. community.zabbix collection’ı var ama onu bir kenara bırakıp API’yi direkt kullanan bir task yazmak bazen daha kontrollü oluyor:

# Ansible playbook'ta kullanmak icin shell task ornegi
# Bu yaklasim, collection bagimliligini ortadan kaldirir

ansible-playbook -i inventory/production site.yml 
  -e "zabbix_url=http://zabbix.sirketiniz.com/zabbix" 
  -e "zabbix_user={{ vault_zabbix_user }}" 
  -e "zabbix_pass={{ vault_zabbix_pass }}" 
  --tags zabbix-register

# Veya playbook icinde Python script cagirabilirsiniz:
# - name: Register host to Zabbix
#   script: scripts/zabbix_register.py
#   environment:
#     ZABBIX_URL: "{{ zabbix_url }}"
#     ZABBIX_USER: "{{ vault_zabbix_user }}"
#     ZABBIX_PASS: "{{ vault_zabbix_pass }}"
#     HOST_IP: "{{ ansible_host }}"
#     HOST_NAME: "{{ inventory_hostname }}"

Pratik İpuçları ve Dikkat Edilmesi Gerekenler

Birkaç yıllık API kullanımından öğrendiğim şeyler:

  • Rate limiting: Büyük toplu işlemlerde time.sleep(0.1) kullanın. Zabbix API’yi flood’lamak sunucuyu yavaşlatıyor, özellikle büyük ortamlarda.
  • Kimlik bilgilerini sakın hardcode etmeyin: os.environ kullanın veya HashiCorp Vault entegrasyonu yapın. Script’i git’e commit etmeden önce iki kez düşünün.
  • API token kullanın (Zabbix 5.4+): User -> API tokens menüsünden token oluşturun, kullanıcı şifresi yerine bunu kullanın. Token rotasyonu çok daha kolay.
  • Loglama zorunlu: Hangi hostun eklendiğini, hangisinin atlandığını mutlaka loglamak şart. Bir ay sonra “Bu host ne zaman eklendi?” sorusu geldiğinde cevabınız hazır olsun.
  • Idempotent yazın: Script’i iki kez çalıştırırsanız ne olur? host_exists() gibi kontroller ekleyin, her seferinde duplicate oluşmasın.
  • Zabbix 6.4 API değişikliği: 6.4’te user.login parametresi user yerine username oldu. Sürüme göre uyumlu kod yazmak için API version kontrolü ekleyin.
  • Hata yönetimi: result None dönüyorsa işlemi durdurun. API yanıtsız kaldığında tüm loop’un çökmesini önlemek için her host için try/except ekleyin.

Sonuç

Zabbix API, izleme altyapınızın yönetimini kökten değiştiriyor. El ile yapılan işlemler hem zaman kaybı hem de hata kaynağı. Bir kez bu script’leri yazdığınızda, yeni sunucu provision etmek ile Zabbix’e kaydetmek aynı pipeline’ın parçası hale geliyor.

Başlangıç için tavsiyem şu: Önce get_zabbix_ids() script’ini çalıştırın, ortamınızı tanıyın. Sonra tek host eklemeyi test edin. Çalışınca CSV import’a geçin. Her adımda dry_run kullanın. Bir kez otomatize ettiğinizde geriye dönmek istemeyeceksiniz, söz.

Bu yazıda kullandığım tüm script’leri bir GitHub repo’suna koyabilir, CI/CD pipeline’ınıza entegre edebilirsiniz. Ansible, Terraform veya Pulumi ile provision ettiğiniz sunucuların otomatik olarak Zabbix’e girmesi, monitoring altyapınızı gerçekten code-driven hale getiriyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir