feat(netbox)!: gut server from 182 to 37 tools
BREAKING CHANGE: Removed circuits, tenancy, VPN, wireless modules entirely. Stripped DCIM, IPAM, virtualization, extras to only essential tools. Deleted NETBOX_ENABLED_MODULES filtering — no longer needed. - Delete circuits.py, tenancy.py, vpn.py, wireless.py - Strip dcim.py to sites, devices, interfaces only (11 tools) - Strip ipam.py to IPs, prefixes, services only (10 tools) - Strip virtualization.py to clusters, VMs, VM interfaces only (10 tools) - Strip extras.py to tags, journal entries only (6 tools) - Remove all module filtering code from config.py and server.py - Rewrite README.md with accurate 37-tool documentation - Update CHANGELOG.md with breaking change entry - Token reduction: ~19,810 → ~3,700 (~81%) Remaining work: Update cmdb-assistant plugin skills (10 files) in follow-up PR Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Extras tools for NetBox MCP Server.
|
||||
|
||||
Covers: Tags, Custom Fields, Custom Links, Webhooks, Journal Entries, and more.
|
||||
Covers: Tags and Journal Entries only.
|
||||
"""
|
||||
import logging
|
||||
from typing import List, Dict, Optional, Any
|
||||
@@ -50,209 +50,6 @@ class ExtrasTools:
|
||||
data['description'] = description
|
||||
return self.client.create(f'{self.base_endpoint}/tags', data)
|
||||
|
||||
async def update_tag(self, id: int, **kwargs) -> Dict:
|
||||
"""Update a tag."""
|
||||
return self.client.patch(f'{self.base_endpoint}/tags', id, kwargs)
|
||||
|
||||
async def delete_tag(self, id: int) -> None:
|
||||
"""Delete a tag."""
|
||||
self.client.delete(f'{self.base_endpoint}/tags', id)
|
||||
|
||||
# ==================== Custom Fields ====================
|
||||
|
||||
async def list_custom_fields(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
type: Optional[str] = None,
|
||||
content_types: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> List[Dict]:
|
||||
"""List all custom fields."""
|
||||
params = {k: v for k, v in {
|
||||
'name': name, 'type': type, 'content_types': content_types, **kwargs
|
||||
}.items() if v is not None}
|
||||
return self.client.list(f'{self.base_endpoint}/custom-fields', params=params)
|
||||
|
||||
async def get_custom_field(self, id: int) -> Dict:
|
||||
"""Get a specific custom field by ID."""
|
||||
return self.client.get(f'{self.base_endpoint}/custom-fields', id)
|
||||
|
||||
async def create_custom_field(
|
||||
self,
|
||||
name: str,
|
||||
content_types: List[str],
|
||||
type: str = 'text',
|
||||
label: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
required: bool = False,
|
||||
filter_logic: str = 'loose',
|
||||
default: Optional[Any] = None,
|
||||
weight: int = 100,
|
||||
validation_minimum: Optional[int] = None,
|
||||
validation_maximum: Optional[int] = None,
|
||||
validation_regex: Optional[str] = None,
|
||||
choice_set: Optional[int] = None,
|
||||
**kwargs
|
||||
) -> Dict:
|
||||
"""Create a new custom field."""
|
||||
data = {
|
||||
'name': name, 'content_types': content_types, 'type': type,
|
||||
'required': required, 'filter_logic': filter_logic, 'weight': weight, **kwargs
|
||||
}
|
||||
for key, val in [
|
||||
('label', label), ('description', description), ('default', default),
|
||||
('validation_minimum', validation_minimum), ('validation_maximum', validation_maximum),
|
||||
('validation_regex', validation_regex), ('choice_set', choice_set)
|
||||
]:
|
||||
if val is not None:
|
||||
data[key] = val
|
||||
return self.client.create(f'{self.base_endpoint}/custom-fields', data)
|
||||
|
||||
async def update_custom_field(self, id: int, **kwargs) -> Dict:
|
||||
"""Update a custom field."""
|
||||
return self.client.patch(f'{self.base_endpoint}/custom-fields', id, kwargs)
|
||||
|
||||
async def delete_custom_field(self, id: int) -> None:
|
||||
"""Delete a custom field."""
|
||||
self.client.delete(f'{self.base_endpoint}/custom-fields', id)
|
||||
|
||||
# ==================== Custom Field Choice Sets ====================
|
||||
|
||||
async def list_custom_field_choice_sets(self, name: Optional[str] = None, **kwargs) -> List[Dict]:
|
||||
"""List all custom field choice sets."""
|
||||
params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None}
|
||||
return self.client.list(f'{self.base_endpoint}/custom-field-choice-sets', params=params)
|
||||
|
||||
async def get_custom_field_choice_set(self, id: int) -> Dict:
|
||||
"""Get a specific custom field choice set by ID."""
|
||||
return self.client.get(f'{self.base_endpoint}/custom-field-choice-sets', id)
|
||||
|
||||
async def create_custom_field_choice_set(
|
||||
self,
|
||||
name: str,
|
||||
extra_choices: List[List[str]],
|
||||
description: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> Dict:
|
||||
"""Create a new custom field choice set."""
|
||||
data = {'name': name, 'extra_choices': extra_choices, **kwargs}
|
||||
if description:
|
||||
data['description'] = description
|
||||
return self.client.create(f'{self.base_endpoint}/custom-field-choice-sets', data)
|
||||
|
||||
async def update_custom_field_choice_set(self, id: int, **kwargs) -> Dict:
|
||||
"""Update a custom field choice set."""
|
||||
return self.client.patch(f'{self.base_endpoint}/custom-field-choice-sets', id, kwargs)
|
||||
|
||||
async def delete_custom_field_choice_set(self, id: int) -> None:
|
||||
"""Delete a custom field choice set."""
|
||||
self.client.delete(f'{self.base_endpoint}/custom-field-choice-sets', id)
|
||||
|
||||
# ==================== Custom Links ====================
|
||||
|
||||
async def list_custom_links(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
content_types: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> List[Dict]:
|
||||
"""List all custom links."""
|
||||
params = {k: v for k, v in {
|
||||
'name': name, 'content_types': content_types, **kwargs
|
||||
}.items() if v is not None}
|
||||
return self.client.list(f'{self.base_endpoint}/custom-links', params=params)
|
||||
|
||||
async def get_custom_link(self, id: int) -> Dict:
|
||||
"""Get a specific custom link by ID."""
|
||||
return self.client.get(f'{self.base_endpoint}/custom-links', id)
|
||||
|
||||
async def create_custom_link(
|
||||
self,
|
||||
name: str,
|
||||
content_types: List[str],
|
||||
link_text: str,
|
||||
link_url: str,
|
||||
enabled: bool = True,
|
||||
new_window: bool = False,
|
||||
weight: int = 100,
|
||||
group_name: Optional[str] = None,
|
||||
button_class: str = 'outline-dark',
|
||||
**kwargs
|
||||
) -> Dict:
|
||||
"""Create a new custom link."""
|
||||
data = {
|
||||
'name': name, 'content_types': content_types,
|
||||
'link_text': link_text, 'link_url': link_url,
|
||||
'enabled': enabled, 'new_window': new_window,
|
||||
'weight': weight, 'button_class': button_class, **kwargs
|
||||
}
|
||||
if group_name:
|
||||
data['group_name'] = group_name
|
||||
return self.client.create(f'{self.base_endpoint}/custom-links', data)
|
||||
|
||||
async def update_custom_link(self, id: int, **kwargs) -> Dict:
|
||||
"""Update a custom link."""
|
||||
return self.client.patch(f'{self.base_endpoint}/custom-links', id, kwargs)
|
||||
|
||||
async def delete_custom_link(self, id: int) -> None:
|
||||
"""Delete a custom link."""
|
||||
self.client.delete(f'{self.base_endpoint}/custom-links', id)
|
||||
|
||||
# ==================== Webhooks ====================
|
||||
|
||||
async def list_webhooks(self, name: Optional[str] = None, **kwargs) -> List[Dict]:
|
||||
"""List all webhooks."""
|
||||
params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None}
|
||||
return self.client.list(f'{self.base_endpoint}/webhooks', params=params)
|
||||
|
||||
async def get_webhook(self, id: int) -> Dict:
|
||||
"""Get a specific webhook by ID."""
|
||||
return self.client.get(f'{self.base_endpoint}/webhooks', id)
|
||||
|
||||
async def create_webhook(
|
||||
self,
|
||||
name: str,
|
||||
payload_url: str,
|
||||
content_types: List[str],
|
||||
type_create: bool = True,
|
||||
type_update: bool = True,
|
||||
type_delete: bool = True,
|
||||
type_job_start: bool = False,
|
||||
type_job_end: bool = False,
|
||||
enabled: bool = True,
|
||||
http_method: str = 'POST',
|
||||
http_content_type: str = 'application/json',
|
||||
additional_headers: Optional[str] = None,
|
||||
body_template: Optional[str] = None,
|
||||
secret: Optional[str] = None,
|
||||
ssl_verification: bool = True,
|
||||
ca_file_path: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> Dict:
|
||||
"""Create a new webhook."""
|
||||
data = {
|
||||
'name': name, 'payload_url': payload_url, 'content_types': content_types,
|
||||
'type_create': type_create, 'type_update': type_update, 'type_delete': type_delete,
|
||||
'type_job_start': type_job_start, 'type_job_end': type_job_end,
|
||||
'enabled': enabled, 'http_method': http_method,
|
||||
'http_content_type': http_content_type, 'ssl_verification': ssl_verification, **kwargs
|
||||
}
|
||||
for key, val in [
|
||||
('additional_headers', additional_headers), ('body_template', body_template),
|
||||
('secret', secret), ('ca_file_path', ca_file_path)
|
||||
]:
|
||||
if val is not None:
|
||||
data[key] = val
|
||||
return self.client.create(f'{self.base_endpoint}/webhooks', data)
|
||||
|
||||
async def update_webhook(self, id: int, **kwargs) -> Dict:
|
||||
"""Update a webhook."""
|
||||
return self.client.patch(f'{self.base_endpoint}/webhooks', id, kwargs)
|
||||
|
||||
async def delete_webhook(self, id: int) -> None:
|
||||
"""Delete a webhook."""
|
||||
self.client.delete(f'{self.base_endpoint}/webhooks', id)
|
||||
|
||||
# ==================== Journal Entries ====================
|
||||
|
||||
async def list_journal_entries(
|
||||
@@ -288,273 +85,3 @@ class ExtrasTools:
|
||||
'comments': comments, 'kind': kind, **kwargs
|
||||
}
|
||||
return self.client.create(f'{self.base_endpoint}/journal-entries', data)
|
||||
|
||||
async def update_journal_entry(self, id: int, **kwargs) -> Dict:
|
||||
"""Update a journal entry."""
|
||||
return self.client.patch(f'{self.base_endpoint}/journal-entries', id, kwargs)
|
||||
|
||||
async def delete_journal_entry(self, id: int) -> None:
|
||||
"""Delete a journal entry."""
|
||||
self.client.delete(f'{self.base_endpoint}/journal-entries', id)
|
||||
|
||||
# ==================== Config Contexts ====================
|
||||
|
||||
async def list_config_contexts(self, name: Optional[str] = None, **kwargs) -> List[Dict]:
|
||||
"""List all config contexts."""
|
||||
params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None}
|
||||
return self.client.list(f'{self.base_endpoint}/config-contexts', params=params)
|
||||
|
||||
async def get_config_context(self, id: int) -> Dict:
|
||||
"""Get a specific config context by ID."""
|
||||
return self.client.get(f'{self.base_endpoint}/config-contexts', id)
|
||||
|
||||
async def create_config_context(
|
||||
self,
|
||||
name: str,
|
||||
data: Dict[str, Any],
|
||||
weight: int = 1000,
|
||||
description: Optional[str] = None,
|
||||
is_active: bool = True,
|
||||
regions: Optional[List[int]] = None,
|
||||
site_groups: Optional[List[int]] = None,
|
||||
sites: Optional[List[int]] = None,
|
||||
locations: Optional[List[int]] = None,
|
||||
device_types: Optional[List[int]] = None,
|
||||
roles: Optional[List[int]] = None,
|
||||
platforms: Optional[List[int]] = None,
|
||||
cluster_types: Optional[List[int]] = None,
|
||||
cluster_groups: Optional[List[int]] = None,
|
||||
clusters: Optional[List[int]] = None,
|
||||
tenant_groups: Optional[List[int]] = None,
|
||||
tenants: Optional[List[int]] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
**kwargs
|
||||
) -> Dict:
|
||||
"""Create a new config context."""
|
||||
context_data = {
|
||||
'name': name, 'data': data, 'weight': weight, 'is_active': is_active, **kwargs
|
||||
}
|
||||
for key, val in [
|
||||
('description', description), ('regions', regions),
|
||||
('site_groups', site_groups), ('sites', sites),
|
||||
('locations', locations), ('device_types', device_types),
|
||||
('roles', roles), ('platforms', platforms),
|
||||
('cluster_types', cluster_types), ('cluster_groups', cluster_groups),
|
||||
('clusters', clusters), ('tenant_groups', tenant_groups),
|
||||
('tenants', tenants), ('tags', tags)
|
||||
]:
|
||||
if val is not None:
|
||||
context_data[key] = val
|
||||
return self.client.create(f'{self.base_endpoint}/config-contexts', context_data)
|
||||
|
||||
async def update_config_context(self, id: int, **kwargs) -> Dict:
|
||||
"""Update a config context."""
|
||||
return self.client.patch(f'{self.base_endpoint}/config-contexts', id, kwargs)
|
||||
|
||||
async def delete_config_context(self, id: int) -> None:
|
||||
"""Delete a config context."""
|
||||
self.client.delete(f'{self.base_endpoint}/config-contexts', id)
|
||||
|
||||
# ==================== Config Templates ====================
|
||||
|
||||
async def list_config_templates(self, name: Optional[str] = None, **kwargs) -> List[Dict]:
|
||||
"""List all config templates."""
|
||||
params = {k: v for k, v in {'name': name, **kwargs}.items() if v is not None}
|
||||
return self.client.list(f'{self.base_endpoint}/config-templates', params=params)
|
||||
|
||||
async def get_config_template(self, id: int) -> Dict:
|
||||
"""Get a specific config template by ID."""
|
||||
return self.client.get(f'{self.base_endpoint}/config-templates', id)
|
||||
|
||||
async def create_config_template(
|
||||
self,
|
||||
name: str,
|
||||
template_code: str,
|
||||
description: Optional[str] = None,
|
||||
environment_params: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
) -> Dict:
|
||||
"""Create a new config template."""
|
||||
data = {'name': name, 'template_code': template_code, **kwargs}
|
||||
if description:
|
||||
data['description'] = description
|
||||
if environment_params:
|
||||
data['environment_params'] = environment_params
|
||||
return self.client.create(f'{self.base_endpoint}/config-templates', data)
|
||||
|
||||
async def update_config_template(self, id: int, **kwargs) -> Dict:
|
||||
"""Update a config template."""
|
||||
return self.client.patch(f'{self.base_endpoint}/config-templates', id, kwargs)
|
||||
|
||||
async def delete_config_template(self, id: int) -> None:
|
||||
"""Delete a config template."""
|
||||
self.client.delete(f'{self.base_endpoint}/config-templates', id)
|
||||
|
||||
# ==================== Export Templates ====================
|
||||
|
||||
async def list_export_templates(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
content_types: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> List[Dict]:
|
||||
"""List all export templates."""
|
||||
params = {k: v for k, v in {
|
||||
'name': name, 'content_types': content_types, **kwargs
|
||||
}.items() if v is not None}
|
||||
return self.client.list(f'{self.base_endpoint}/export-templates', params=params)
|
||||
|
||||
async def get_export_template(self, id: int) -> Dict:
|
||||
"""Get a specific export template by ID."""
|
||||
return self.client.get(f'{self.base_endpoint}/export-templates', id)
|
||||
|
||||
async def create_export_template(
|
||||
self,
|
||||
name: str,
|
||||
content_types: List[str],
|
||||
template_code: str,
|
||||
description: Optional[str] = None,
|
||||
mime_type: str = 'text/plain',
|
||||
file_extension: Optional[str] = None,
|
||||
as_attachment: bool = True,
|
||||
**kwargs
|
||||
) -> Dict:
|
||||
"""Create a new export template."""
|
||||
data = {
|
||||
'name': name, 'content_types': content_types,
|
||||
'template_code': template_code, 'mime_type': mime_type,
|
||||
'as_attachment': as_attachment, **kwargs
|
||||
}
|
||||
if description:
|
||||
data['description'] = description
|
||||
if file_extension:
|
||||
data['file_extension'] = file_extension
|
||||
return self.client.create(f'{self.base_endpoint}/export-templates', data)
|
||||
|
||||
async def update_export_template(self, id: int, **kwargs) -> Dict:
|
||||
"""Update an export template."""
|
||||
return self.client.patch(f'{self.base_endpoint}/export-templates', id, kwargs)
|
||||
|
||||
async def delete_export_template(self, id: int) -> None:
|
||||
"""Delete an export template."""
|
||||
self.client.delete(f'{self.base_endpoint}/export-templates', id)
|
||||
|
||||
# ==================== Saved Filters ====================
|
||||
|
||||
async def list_saved_filters(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
content_types: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> List[Dict]:
|
||||
"""List all saved filters."""
|
||||
params = {k: v for k, v in {
|
||||
'name': name, 'content_types': content_types, **kwargs
|
||||
}.items() if v is not None}
|
||||
return self.client.list(f'{self.base_endpoint}/saved-filters', params=params)
|
||||
|
||||
async def get_saved_filter(self, id: int) -> Dict:
|
||||
"""Get a specific saved filter by ID."""
|
||||
return self.client.get(f'{self.base_endpoint}/saved-filters', id)
|
||||
|
||||
async def create_saved_filter(
|
||||
self,
|
||||
name: str,
|
||||
slug: str,
|
||||
content_types: List[str],
|
||||
parameters: Dict[str, Any],
|
||||
description: Optional[str] = None,
|
||||
weight: int = 100,
|
||||
enabled: bool = True,
|
||||
shared: bool = True,
|
||||
**kwargs
|
||||
) -> Dict:
|
||||
"""Create a new saved filter."""
|
||||
data = {
|
||||
'name': name, 'slug': slug, 'content_types': content_types,
|
||||
'parameters': parameters, 'weight': weight,
|
||||
'enabled': enabled, 'shared': shared, **kwargs
|
||||
}
|
||||
if description:
|
||||
data['description'] = description
|
||||
return self.client.create(f'{self.base_endpoint}/saved-filters', data)
|
||||
|
||||
async def update_saved_filter(self, id: int, **kwargs) -> Dict:
|
||||
"""Update a saved filter."""
|
||||
return self.client.patch(f'{self.base_endpoint}/saved-filters', id, kwargs)
|
||||
|
||||
async def delete_saved_filter(self, id: int) -> None:
|
||||
"""Delete a saved filter."""
|
||||
self.client.delete(f'{self.base_endpoint}/saved-filters', id)
|
||||
|
||||
# ==================== Image Attachments ====================
|
||||
|
||||
async def list_image_attachments(
|
||||
self,
|
||||
object_type: Optional[str] = None,
|
||||
object_id: Optional[int] = None,
|
||||
**kwargs
|
||||
) -> List[Dict]:
|
||||
"""List all image attachments."""
|
||||
params = {k: v for k, v in {
|
||||
'object_type': object_type, 'object_id': object_id, **kwargs
|
||||
}.items() if v is not None}
|
||||
return self.client.list(f'{self.base_endpoint}/image-attachments', params=params)
|
||||
|
||||
async def get_image_attachment(self, id: int) -> Dict:
|
||||
"""Get a specific image attachment by ID."""
|
||||
return self.client.get(f'{self.base_endpoint}/image-attachments', id)
|
||||
|
||||
async def delete_image_attachment(self, id: int) -> None:
|
||||
"""Delete an image attachment."""
|
||||
self.client.delete(f'{self.base_endpoint}/image-attachments', id)
|
||||
|
||||
# ==================== Object Changes (Audit Log) ====================
|
||||
|
||||
async def list_object_changes(
|
||||
self,
|
||||
user_id: Optional[int] = None,
|
||||
changed_object_type: Optional[str] = None,
|
||||
changed_object_id: Optional[int] = None,
|
||||
action: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> List[Dict]:
|
||||
"""List all object changes (audit log)."""
|
||||
params = {k: v for k, v in {
|
||||
'user_id': user_id, 'changed_object_type': changed_object_type,
|
||||
'changed_object_id': changed_object_id, 'action': action, **kwargs
|
||||
}.items() if v is not None}
|
||||
return self.client.list(f'{self.base_endpoint}/object-changes', params=params)
|
||||
|
||||
async def get_object_change(self, id: int) -> Dict:
|
||||
"""Get a specific object change by ID."""
|
||||
return self.client.get(f'{self.base_endpoint}/object-changes', id)
|
||||
|
||||
# ==================== Scripts ====================
|
||||
|
||||
async def list_scripts(self, **kwargs) -> List[Dict]:
|
||||
"""List all available scripts."""
|
||||
return self.client.list(f'{self.base_endpoint}/scripts', params=kwargs)
|
||||
|
||||
async def get_script(self, id: str) -> Dict:
|
||||
"""Get a specific script by ID."""
|
||||
return self.client.get(f'{self.base_endpoint}/scripts', id)
|
||||
|
||||
async def run_script(self, id: str, data: Dict[str, Any], commit: bool = True) -> Dict:
|
||||
"""Run a script with the provided data."""
|
||||
payload = {'data': data, 'commit': commit}
|
||||
return self.client.create(f'{self.base_endpoint}/scripts/{id}', payload)
|
||||
|
||||
# ==================== Reports ====================
|
||||
|
||||
async def list_reports(self, **kwargs) -> List[Dict]:
|
||||
"""List all available reports."""
|
||||
return self.client.list(f'{self.base_endpoint}/reports', params=kwargs)
|
||||
|
||||
async def get_report(self, id: str) -> Dict:
|
||||
"""Get a specific report by ID."""
|
||||
return self.client.get(f'{self.base_endpoint}/reports', id)
|
||||
|
||||
async def run_report(self, id: str) -> Dict:
|
||||
"""Run a report."""
|
||||
return self.client.create(f'{self.base_endpoint}/reports/{id}', {})
|
||||
|
||||
Reference in New Issue
Block a user