Searching also for DEV-Guideline for snapins

I need some guidance on developing a snap-in

because I want to develop a snap-in for ticket systems (SNOW, Jira, etc.), but I can’t find any resources that help me with built-in snap-ins.

checkmk/cmk/gui/sidebar/_snapin at master · Checkmk/checkmk

here my draft - but didn`t will be shown in the GUI

snow_snap.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ~/local/lib/python3/cmk/gui/plugins/sidebar/servicenow_tickets.py

"""
ServiceNow Tickets Sidebar Snapin for Checkmk 2.4+

Displays incident statistics from ServiceNow.

Author: BH2005
Version: 0.2.0
Date: 2025-02-11
"""

from typing import Optional, Dict, Any
import json
import urllib.request
import urllib.error
import base64
from datetime import datetime

# Checkmk 2.4 Imports - KORREKT ?????
import cmk.gui.sites as sites
from cmk.gui.i18n import _
from cmk.gui.htmllib.html import html
from cmk.gui.sidebar._snapin import snapin_registry
from cmk.gui.sidebar._snapin._base import CustomizableSidebarSnapin
from cmk.gui.sidebar._snapin._helpers import snapin_width


@snapin_registry.register
class ServiceNowTicketsSnapin(CustomizableSidebarSnapin):
    """
    ServiceNow Tickets Sidebar Snapin
    
    Shows incident statistics from ServiceNow.
    """
    
    @staticmethod
    def type_name():
        return "servicenow_tickets"
    
    @classmethod
    def title(cls):
        return _("ServiceNow Tickets")
    
    @classmethod
    def description(cls):
        return _("Display ServiceNow incident statistics")
    
    @classmethod
    def refresh_regularly(cls):
        return True
    
    @classmethod
    def refresh_interval(cls):
        return 60
    
    def show(self):
        """Main render function"""
        width = snapin_width()
        
        html.open_div(class_="servicenow_tickets", style=f"width: {width}px;")
        
        # Header
        html.open_div(style="padding: 8px; border-bottom: 2px solid #0084c8;")
        html.h3(_("ServiceNow"), style="margin: 0; font-size: 14px;")
        html.close_div()
        
        # Get configuration
        config = self._get_config()
        
        if not config.get('enabled'):
            self._render_not_configured()
            html.close_div()
            return
        
        # Fetch stats
        stats = self._fetch_stats(config)
        
        if stats and 'error' in stats:
            self._render_error(stats['error'])
        elif stats:
            self._render_stats(stats, config)
        else:
            self._render_loading()
        
        html.close_div()
    
    def _get_config(self) -> Dict[str, Any]:
        """Get ServiceNow configuration"""
        default_config = {
            'enabled': False,
            'instance': '',
            'username': '',
            'password': '',
            'verify_ssl': True,
        }
        
        try:
            from cmk.gui.config import active_config
            config = getattr(active_config, 'servicenow_config', default_config)
            return {**default_config, **config}
        except Exception:
            return default_config
    
    def _fetch_stats(self, config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        """Fetch incident statistics from ServiceNow"""
        if not config.get('instance') or not config.get('username'):
            return {'error': 'Not configured'}
        
        try:
            base_url = self._get_base_url(config['instance'])
            
            # Query for each state
            stats = {
                'new': self._count_incidents(base_url, config, 'state=1'),
                'in_progress': self._count_incidents(base_url, config, 'state=2'),
                'on_hold': self._count_incidents(base_url, config, 'state=3'),
                'resolved': self._count_incidents(base_url, config, 'state=6'),
            }
            
            stats['total'] = sum(stats.values())
            return stats
            
        except Exception as e:
            return {'error': str(e)}
    
    def _get_base_url(self, instance: str) -> str:
        """Build ServiceNow base URL"""
        if instance.startswith('http'):
            return instance.rstrip('/')
        return f"https://{instance}.service-now.com"
    
    def _count_incidents(self, base_url: str, config: Dict[str, Any], query: str) -> int:
        """Count incidents with specific query"""
        url = f"{base_url}/api/now/table/incident?sysparm_query=active=true^{query}&sysparm_limit=1"
        
        req = urllib.request.Request(url)
        req.add_header('Accept', 'application/json')
        
        # Basic auth
        credentials = base64.b64encode(
            f"{config['username']}:{config['password']}".encode()
        ).decode()
        req.add_header('Authorization', f"Basic {credentials}")
        
        # SSL context
        context = None
        if not config.get('verify_ssl', True):
            import ssl
            context = ssl._create_unverified_context()
        
        try:
            response = urllib.request.urlopen(req, timeout=5, context=context)
            # ServiceNow returns X-Total-Count header
            return int(response.headers.get('X-Total-Count', 0))
        except Exception:
            return 0
    
    def _render_stats(self, stats: Dict[str, Any], config: Dict[str, Any]):
        """Render statistics"""
        html.open_div(style="padding: 10px;")
        
        # Status with colors
        html.open_table(style="width: 100%; font-size: 12px;")
        
        items = [
            ('new', _("New"), '#0084c8'),
            ('in_progress', _("In Progress"), '#ffc107'),
            ('on_hold', _("On Hold"), '#ffcc00'),
            ('resolved', _("Resolved"), '#28a745'),
        ]
        
        for key, label, color in items:
            count = stats.get(key, 0)
            if count > 0:
                html.open_tr()
                html.td(label + ":", style="color: #666;")
                html.td(
                    str(count),
                    style=f"font-weight: bold; color: {color}; text-align: right;"
                )
                html.close_tr()
        
        # Total
        html.open_tr(style="border-top: 1px solid #ddd;")
        html.td(_("Total Active:"), style="font-weight: bold; padding-top: 5px;")
        html.td(
            str(stats.get('total', 0)),
            style="font-weight: bold; text-align: right; padding-top: 5px;"
                )
        html.close_tr()
        
        html.close_table()
        
        # Links
        base_url = self._get_base_url(config['instance'])
        
        html.open_div(style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #ddd;")
        html.div(_("Quick Links"), style="font-size: 11px; color: #666; margin-bottom: 5px;")
        
        html.open_ul(style="margin: 0; padding: 0; list-style: none;")
        
        links = [
            (_("Dashboard"), f"{base_url}/now/nav/ui/classic/params/target/home"),
            (_("Create Incident"), f"{base_url}/incident.do?sys_id=-1"),
            (_("My Incidents"), f"{base_url}/incident_list.do?sysparm_query=active=true"),
            (_("All Open"), f"{base_url}/incident_list.do?sysparm_query=active=true^stateIN1,2,3"),
        ]
        
        for label, url in links:
            html.open_li(style="margin: 3px 0;")
            html.a(
                label,
                href=url,
                target="_blank",
                style="font-size: 11px; color: #0084c8; text-decoration: none;"
            )
            html.close_li()
        
        html.close_ul()
        html.close_div()
        
        # Footer
        html.div(
            _("Updated: %s") % datetime.now().strftime("%H:%M:%S"),
            style="font-size: 10px; color: #999; text-align: right; margin-top: 10px;"
        )
        
        html.close_div()
    
    def _render_not_configured(self):
        """Render not configured message"""
        html.open_div(style="padding: 10px; background: #fff3cd; margin: 10px; border-radius: 4px;")
        html.write_text(_("ServiceNow not configured."))
        html.close_div()
    
    def _render_error(self, error: str):
        """Render error message"""
        html.open_div(style="padding: 10px; background: #f8d7da; margin: 10px; border-radius: 4px; font-size: 11px;")
        html.write_text(_("Error: %s") % error)
        html.close_div()
    
    def _render_loading(self):
        """Render loading message"""
        html.open_div(style="padding: 10px; text-align: center; color: #666; font-style: italic;")
        html.write_text(_("Loading..."))
        html.close_div()

2 Likes

now I have understand the DEV for that

CMK-exchange/snapins/cmk-ticket-system/ticket_system_snapin_package at main · bh2005/CMK-exchange

test was a snapin for the ticket-system:

jbelkacemi - Checkmk Exchange

next will follow for:

JIRA, SNOW, GLPI, ZAMMAD

I also try to write a guideline for that

Checkmk 2.4 Sidebar Snapin Development Guideline

Complete guide for developing sidebar snapins for Checkmk 2.4+, based on the Ticket System Snapin development experience.


:clipboard: Table of Contents

  1. Prerequisites
  2. Project Structure
  3. Core Concepts
  4. Minimum Requirements
  5. Step-by-Step Development
  6. API Reference
  7. Best Practices
  8. Common Pitfalls
  9. Testing Strategy
  10. Deployment
  11. Troubleshooting
  12. Advanced Topics

Prerequisites

Environment

  • Checkmk 2.4.0 or higher
  • Python 3.12+ (included with Checkmk 2.4)
  • OMD Site with write access
  • Basic Python knowledge
  • Understanding of Checkmk’s plugin architecture

Tools

# Development tools
- Text editor (VSCode, PyCharm, vim)
- Git for version control
- Python linter (pylint, flake8)
- SQLite browser (for database snapins)

Project Structure

File System Layout

~/local/lib/python3/
└── cmk/
    ├── __init__.py              # Required!
    └── gui/
        ├── __init__.py          # Required!
        └── plugins/
            ├── __init__.py      # Required!
            └── sidebar/
                ├── __init__.py  # Required!
                └── my_snapin.py # Your snapin

Important Paths

Path Purpose
~/local/lib/python3/cmk/gui/plugins/sidebar/ Snapin location
~/var/log/web.log Error logs
~/tmp/check_mk/ Temporary files
/omd/sites/<site>/lib/python3/cmk/gui/sidebar/ Core snapin code (read-only)

Core Concepts

What is a Sidebar Snapin?

A sidebar snapin is a modular UI component that appears in the Checkmk sidebar, providing:

  • Quick information display
  • Navigation shortcuts
  • Status monitoring
  • Action buttons

Snapin Lifecycle

1. Registration   → @snapin_registry.register
2. Discovery      → Checkmk scans plugins directory
3. Instantiation  → Class is instantiated
4. Rendering      → show() method is called
5. Auto-refresh   → Periodic re-rendering (if enabled)

Minimum Requirements

Required Imports

from cmk.gui.i18n import _
from cmk.gui.htmllib.html import html
from cmk.gui.sidebar._snapin import snapin_registry, SidebarSnapin

Minimal Snapin Template

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Minimal Snapin Template for Checkmk 2.4+
"""

from cmk.gui.i18n import _
from cmk.gui.htmllib.html import html
from cmk.gui.sidebar._snapin import snapin_registry, SidebarSnapin


@snapin_registry.register
class MyMinimalSnapin(SidebarSnapin):
    
    @staticmethod
    def type_name():
        """Unique identifier (lowercase, no spaces)"""
        return "my_snapin"
    
    @classmethod
    def title(cls):
        """Display name in snapin list"""
        return _("My Snapin")
    
    @classmethod
    def description(cls):
        """Tooltip description"""
        return _("Short description of what this snapin does")
    
    def show(self):
        """Main render function - REQUIRED"""
        html.write_text("Hello World!")

Mandatory Methods

Method Type Returns Purpose
type_name() @staticmethod str Unique ID
title() @classmethod str Display name
description() @classmethod str Description
show() instance None Render content

Step-by-Step Development

Phase 1: Planning

Define your snapin’s purpose:

  • :white_check_mark: What information will it display?
  • :white_check_mark: What data sources will it use?
  • :white_check_mark: What interactions will it provide?
  • :white_check_mark: How often should it refresh?

Example:

Purpose: Display ticket system statistics
Data Source: SQLite database
Interactions: Click to filter tickets
Refresh: Every 30 seconds

Phase 2: Basic Structure

1. Create the file:

cd ~/local/lib/python3/cmk/gui/plugins/sidebar
touch my_snapin.py

2. Add minimal template:

from cmk.gui.i18n import _
from cmk.gui.htmllib.html import html
from cmk.gui.sidebar._snapin import snapin_registry, SidebarSnapin

@snapin_registry.register
class MySnapin(SidebarSnapin):
    
    @staticmethod
    def type_name():
        return "my_snapin"
    
    @classmethod
    def title(cls):
        return _("My Snapin")
    
    @classmethod
    def description(cls):
        return _("Description")
    
    def show(self):
        html.write_text("Static test")

3. Test immediately:

python3 -m py_compile my_snapin.py
omd reload apache

Phase 3: Add Data Source

Option A: Database Access

import sqlite3
from pathlib import Path
import os

def _get_data(self):
    db_path = Path(os.getenv("OMD_ROOT", "")) / "var/data.db"
    
    if not db_path.exists():
        return None
    
    conn = sqlite3.connect(str(db_path), timeout=5)
    cursor = conn.cursor()
    cursor.execute("SELECT COUNT(*) FROM table")
    result = cursor.fetchone()[0]
    conn.close()
    
    return result

Option B: REST API Access

import urllib.request
import json

def _fetch_api_data(self):
    try:
        url = "https://api.example.com/stats"
        response = urllib.request.urlopen(url, timeout=5)
        data = json.loads(response.read().decode('utf-8'))
        return data
    except Exception:
        return None

Option C: Livestatus Access

import cmk.gui.sites as sites

def _get_host_stats(self):
    try:
        query = "GET hosts\nColumns: state\nStats: state = 0\nStats: state = 1\nStats: state = 2"
        result = sites.live().query(query)
        return result
    except Exception:
        return None

Phase 4: Render Content

Basic HTML Structure:

def show(self):
    # Container
    html.open_div(style="padding: 10px;")
    
    # Header
    html.open_div(style="font-weight: bold; margin-bottom: 10px;")
    html.write_text("Title")
    html.close_div()
    
    # Content
    data = self._get_data()
    
    if data is None:
        html.write_text("No data available")
    else:
        html.open_table(style="width: 100%;")
        html.open_tr()
        html.td("Label:")
        html.td(str(data), style="text-align: right;")
        html.close_tr()
        html.close_table()
    
    # Close container
    html.close_div()

Phase 5: Add Interactivity

Clickable Elements:

# Clickable header
html.a(
    "Dashboard",
    href="view.py?view_name=myview",
    style="text-decoration: none; color: #0084c8;",
    title=_("Open Dashboard")
)

# Clickable row
html.open_tr(
    style="cursor: pointer;",
    onclick="location.href='view.py?view_name=myview&filter=active'"
)
html.td("Active:")
html.td("5")
html.close_tr()

Action Buttons:

html.open_div(style="margin-top: 10px;")
html.a(
    "➕ Create New",
    href="wato.py?mode=create",
    style="color: #0084c8; text-decoration: none;"
)
html.close_div()

Phase 6: Add Auto-Refresh

@classmethod
def refresh_regularly(cls):
    """Enable auto-refresh"""
    return True

@classmethod
def refresh_interval(cls):
    """Refresh interval in seconds"""
    return 30  # Refresh every 30 seconds

Phase 7: Error Handling

def show(self):
    html.open_div(style="padding: 10px;")
    
    try:
        data = self._get_data()
        
        if data is None:
            # User-friendly error
            html.open_div(style="background: #fff3cd; padding: 8px; border-radius: 4px;")
            html.write_text(_("Data source not available"))
            html.close_div()
        else:
            # Render data
            self._render_stats(data)
            
    except Exception as e:
        # Error display
        html.open_div(style="background: #f8d7da; padding: 8px; border-radius: 4px;")
        html.write_text(_("Error: "))
        html.write_text(str(e))
        html.close_div()
    
    html.close_div()

API Reference

HTML Methods

Text Output

html.write_text("Simple text")           # Plain text
html.write_html("<b>Bold</b>")          # Raw HTML (use with caution!)
html.br()                                # Line break
html.hr()                                # Horizontal rule

Structure Elements

# Div
html.open_div(style="padding: 10px;")
html.write_text("Content")
html.close_div()

# Table
html.open_table(style="width: 100%;")
html.open_tr()
html.td("Label:", style="color: #666;")
html.td("Value", style="text-align: right;")
html.close_tr()
html.close_table()

# List
html.open_ul(style="margin: 0; padding: 0;")
html.open_li()
html.write_text("Item 1")
html.close_li()
html.close_ul()

Links

# Simple link
html.a("Click here", href="view.py?view=myview")

# Styled link
html.a(
    "Dashboard",
    href="wato.py?mode=dashboard",
    style="color: #0084c8; text-decoration: none;",
    title=_("Tooltip text"),
    target="_blank"  # Open in new tab
)

Common Patterns

# Clickable row
html.open_tr(
    style="cursor: pointer;",
    onclick="location.href='url.py'"
)

# Colored text
html.span("Text", style="color: #28a745; font-weight: bold;")

# Icon with text
html.write_text("🎫 ")
html.write_text("Tickets")

Important HTML Rules

:cross_mark: WRONG:

html.td(style="padding: 5px;")              # Missing content!
html.text("Hello")                          # Method doesn't exist!
html.p("Paragraph")                         # Method doesn't exist!

:white_check_mark: CORRECT:

html.td("", style="padding: 5px;")          # Empty string as content
html.write_text("Hello")                    # Correct method
html.open_div()                             # Use div instead
html.write_text("Paragraph")
html.close_div()

Internationalization (i18n)

from cmk.gui.i18n import _

# Wrap all user-visible text
html.write_text(_("Status"))               # Will be translated
html.write_text(_("Open: %d") % count)     # With variables

# Don't translate:
# - Technical IDs (type_name)
# - Log messages
# - Internal strings

Best Practices

Code Organization

class MySnapin(SidebarSnapin):
    # 1. Required static/class methods first
    @staticmethod
    def type_name():
        return "my_snapin"
    
    @classmethod
    def title(cls):
        return _("Title")
    
    @classmethod
    def refresh_regularly(cls):
        return True
    
    # 2. Private helper methods
    def _get_data(self):
        """Fetch data from source"""
        pass
    
    def _render_stats(self, data):
        """Render statistics table"""
        pass
    
    # 3. Main show method last
    def show(self):
        """Main render function"""
        pass

Performance Optimization

# ✅ GOOD: Use connection pooling
def _get_db_connection(self):
    if not hasattr(self, '_conn'):
        self._conn = sqlite3.connect(db_path)
    return self._conn

# ✅ GOOD: Cache expensive operations
def _get_cached_data(self):
    cache_key = 'my_data'
    if cache_key in self._cache:
        return self._cache[cache_key]
    
    data = self._fetch_data()
    self._cache[cache_key] = data
    return data

# ✅ GOOD: Use timeouts
response = urllib.request.urlopen(url, timeout=5)

# ✅ GOOD: Limit query results
cursor.execute("SELECT * FROM table LIMIT 100")

# ❌ BAD: No timeout
response = urllib.request.urlopen(url)  # Can hang forever

# ❌ BAD: Fetch all data
cursor.execute("SELECT * FROM huge_table")  # Can be very slow

Security

# ✅ GOOD: Parameterized queries
cursor.execute("SELECT * FROM tickets WHERE status = ?", (status,))

# ✅ GOOD: Read-only database access
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)

# ✅ GOOD: Timeout protection
conn = sqlite3.connect(db_path, timeout=5)

# ✅ GOOD: Escape user input
from html import escape
html.write_text(escape(user_input))

# ❌ BAD: SQL injection vulnerable
cursor.execute(f"SELECT * FROM tickets WHERE status = '{status}'")

# ❌ BAD: No timeout
conn = sqlite3.connect(db_path)  # Can hang

Error Messages

# ✅ GOOD: User-friendly messages
if not db_path.exists():
    html.write_text(_("Database not found. Please install the Ticket System."))

# ✅ GOOD: Actionable errors
html.write_text(_("Connection failed. Check network settings."))

# ❌ BAD: Technical jargon
html.write_text("sqlite3.OperationalError: no such table")

# ❌ BAD: No context
html.write_text("Error")

Common Pitfalls

1. Import Errors

:cross_mark: WRONG (Old API):

from cmk.gui.plugins.sidebar.utils import SidebarSnapin

:white_check_mark: CORRECT (Checkmk 2.4):

from cmk.gui.sidebar._snapin import SidebarSnapin

2. Method Decorators

:cross_mark: WRONG:

def refresh_regularly(self):  # Instance method
    return True

:white_check_mark: CORRECT:

@classmethod
def refresh_regularly(cls):  # Class method
    return True

3. HTML Method Usage

:cross_mark: WRONG:

html.td(style="padding: 5px;")  # Missing content

:white_check_mark: CORRECT:

html.td("", style="padding: 5px;")  # Empty string

4. Missing init.py

:cross_mark: WRONG:

~/local/lib/python3/cmk/gui/plugins/sidebar/my_snapin.py
# Missing __init__.py files!

:white_check_mark: CORRECT:

~/local/lib/python3/cmk/__init__.py
~/local/lib/python3/cmk/gui/__init__.py
~/local/lib/python3/cmk/gui/plugins/__init__.py
~/local/lib/python3/cmk/gui/plugins/sidebar/__init__.py
~/local/lib/python3/cmk/gui/plugins/sidebar/my_snapin.py

5. Cache Issues

:cross_mark: WRONG:

# Only reload Apache
omd reload apache  # Python cache remains!

:white_check_mark: CORRECT:

# Clear cache AND reload
find ~/local/lib/python3 -name "__pycache__" -exec rm -rf {} + 2>/dev/null
omd reload apache

Testing Strategy

Phase 1: Syntax Validation

# Python syntax check
python3 -m py_compile my_snapin.py

# If syntax errors:
python3 my_snapin.py  # Shows detailed error

Phase 2: Import Testing

# test_import.py
import sys
import os
sys.path.insert(0, os.environ['OMD_ROOT'] + '/local/lib/python3')

try:
    from cmk.gui.plugins.sidebar.my_snapin import MySnapin
    print(f"✓ Import successful")
    print(f"  Type: {MySnapin.type_name()}")
    print(f"  Title: {MySnapin.title()}")
except Exception as e:
    print(f"✗ Import failed: {e}")
    import traceback
    traceback.print_exc()

Phase 3: Data Source Testing

# test_data.py - Standalone test for data fetching
import sqlite3
from pathlib import Path
import os

db_path = Path(os.getenv("OMD_ROOT", "")) / "var/data.db"

if db_path.exists():
    conn = sqlite3.connect(str(db_path))
    cursor = conn.cursor()
    cursor.execute("SELECT COUNT(*) FROM table")
    result = cursor.fetchone()
    print(f"✓ Query result: {result[0]}")
    conn.close()
else:
    print(f"✗ Database not found: {db_path}")

Phase 4: Registration Testing

# test_registration.py
import sys
import os
sys.path.insert(0, os.environ['OMD_ROOT'] + '/local/lib/python3')
sys.path.insert(0, os.environ['OMD_ROOT'] + '/lib/python3')

from cmk.gui.sidebar._snapin import snapin_registry

# Before import
before = set(snapin_registry.keys())

# Import your snapin
from cmk.gui.plugins.sidebar.my_snapin import MySnapin

# After import
after = set(snapin_registry.keys())

new_snapins = after - before
if new_snapins:
    print(f"✓ Registered: {new_snapins}")
else:
    print(f"✗ Not registered!")

Phase 5: GUI Testing

# 1. Clear cache
find ~/local/lib/python3 -name "__pycache__" -exec rm -rf {} + 2>/dev/null

# 2. Reload Apache
omd reload apache

# 3. Check logs
tail -f ~/var/log/web.log | grep -i "my_snapin\|error"

# 4. Test in browser
# - Open Checkmk GUI
# - Click "Add snapin" (+)
# - Search for your snapin
# - Add it
# - Check for errors

Deployment

Development Workflow

# 1. Edit code
vim ~/local/lib/python3/cmk/gui/plugins/sidebar/my_snapin.py

# 2. Test syntax
python3 -m py_compile my_snapin.py

# 3. Clear cache
find ~/local/lib/python3 -name "__pycache__" -exec rm -rf {} + 2>/dev/null

# 4. Reload
omd reload apache

# 5. Test in GUI
# 6. Check logs
tail ~/var/log/web.log

Version Control

# Git workflow
cd ~/local/lib/python3/cmk/gui/plugins/sidebar/

git init
git add my_snapin.py
git commit -m "Initial version"

# After changes
git add my_snapin.py
git commit -m "Added auto-refresh"
git tag v1.0.0

Distribution

Option 1: Standalone File

# Single file distribution
cp my_snapin.py ~/export/my_snapin.py

# Users install:
cp my_snapin.py ~/local/lib/python3/cmk/gui/plugins/sidebar/
omd reload apache

Option 2: Package

# Create package
mkdir -p my_snapin_package
cp my_snapin.py my_snapin_package/
cp README.md my_snapin_package/
cp INSTALL.md my_snapin_package/
tar -czf my_snapin.tar.gz my_snapin_package/

Option 3: MKP Package (Advanced)

# Create MKP package (Checkmk's package format)
# This requires additional setup and configuration
# See Checkmk documentation for MKP creation

Troubleshooting

Snapin Not Appearing

Issue: Snapin doesn’t show in “Add snapin” list

Solutions:

  1. Check init.py files
  2. Verify Python syntax
  3. Clear cache completely
  4. Restart site (not just reload)
  5. Check logs for import errors
# Debug script
python3 << 'EOF'
import sys, os
sys.path.insert(0, os.environ['OMD_ROOT'] + '/local/lib/python3')
from cmk.gui.sidebar._snapin import snapin_registry
from cmk.gui.plugins.sidebar.my_snapin import MySnapin
print(f"Registered: {MySnapin.type_name() in snapin_registry}")
EOF

Shows Error in GUI

Issue: Snapin appears but shows error

Solutions:

  1. Check web.log for Python errors
  2. Test data source separately
  3. Add try-except blocks
  4. Verify HTML method usage
# Live error monitoring
tail -f ~/var/log/web.log | grep -A 10 "my_snapin"

Doesn’t Refresh

Issue: Snapin doesn’t auto-refresh

Solutions:

  1. Verify refresh_regularly() returns True
  2. Check refresh_interval() is @classmethod
  3. Clear browser cache
  4. Check if refresh is disabled site-wide

Performance Issues

Issue: Snapin loads slowly

Solutions:

  1. Add query timeouts
  2. Limit result sets
  3. Cache expensive operations
  4. Profile slow queries
import time

def show(self):
    start = time.time()
    # ... your code ...
    elapsed = time.time() - start
    print(f"Render time: {elapsed:.2f}s")

Advanced Topics

Custom Permissions

@classmethod
def allowed_roles(cls):
    """Restrict to specific roles"""
    return ["admin", "user"]  # Empty list = all roles

Custom CSS (Deprecated in 2.4)

# Note: Custom styles() method is deprecated
# Use inline styles instead

def show(self):
    html.open_div(style="""
        background: #f5f5f5;
        padding: 10px;
        border-radius: 4px;
    """)
    # Content
    html.close_div()

Multi-Site Support

import cmk.gui.sites as sites

def _get_multisite_stats(self):
    """Get stats from all sites"""
    stats = {}
    
    for site_id, site_status in sites.states().items():
        if site_status.get("state") == "online":
            query = "GET hosts\nStats: state = 0"
            result = sites.live().query(query)
            stats[site_id] = result[0][0]
    
    return stats

Configuration Options

from cmk.gui.config import active_config

def _get_config(self):
    """Get custom configuration"""
    default_config = {
        'enabled': True,
        'threshold': 10,
        'url': '',
    }
    
    try:
        config = getattr(active_config, 'my_snapin_config', default_config)
        return {**default_config, **config}
    except Exception:
        return default_config

Users configure in:

# ~/etc/check_mk/multisite.d/wato/my_snapin.mk
my_snapin_config = {
    'enabled': True,
    'threshold': 20,
    'url': 'https://api.example.com',
}

JavaScript Integration

def show(self):
    # Add JavaScript for dynamic behavior
    html.write_html("""
        <script>
        function updateCounter() {
            console.log('Counter updated');
        }
        </script>
    """)
    
    html.open_div(onclick="updateCounter()")
    html.write_text("Click me")
    html.close_div()

Checklist

Development Checklist

  • Created file in correct location
  • All init.py files present
  • Required methods implemented
  • Imports are correct (Checkmk 2.4 API)
  • Python syntax is valid
  • Error handling added
  • User-friendly error messages
  • All text is translatable (_())
  • Code follows best practices
  • Performance optimized
  • Security measures in place

Testing Checklist

  • Syntax validation passed
  • Import test successful
  • Data source test works
  • Registration verified
  • Appears in GUI list
  • Adds to sidebar successfully
  • Displays content correctly
  • No errors in web.log
  • Auto-refresh works
  • Clickable elements work
  • Tested with no data
  • Tested with error conditions

Deployment Checklist

  • Version control set up
  • README documentation written
  • Installation guide created
  • Screenshot/demo prepared
  • License added
  • Code commented
  • Tested on clean system
  • Package created
  • Distribution method chosen

Resources

Official Documentation

Community

Examples

  • Built-in Snapins: /omd/sites/<site>/lib/python3/cmk/gui/sidebar/
  • Checkmk Exchange: Search for “snapin” or “sidebar”
  • This Repository: Ticket System Snapin as reference

Conclusion

This guideline covers the complete development process for Checkmk 2.4 sidebar snapins, from initial setup to deployment. Follow the phases step-by-step, test thoroughly, and refer to the API reference and best practices sections as needed.

Key Takeaways:

  1. :white_check_mark: Use correct Checkmk 2.4 imports
  2. :white_check_mark: Always include init.py files
  3. :white_check_mark: Implement all required methods
  4. :white_check_mark: Use @classmethod for class methods
  5. :white_check_mark: Test incrementally at each phase
  6. :white_check_mark: Handle errors gracefully
  7. :white_check_mark: Optimize for performance
  8. :white_check_mark: Document your code

Happy Snapin Development! :rocket:


Document Version: 1.0.0
Last Updated: 2026-02-13
Tested with: Checkmk 2.4.0p1
Author: BH2005 - Based on Ticket System Snapin development experience

5 Likes