Closes #19973: nbshell improvements (#19995)

This commit is contained in:
Jeremy Stretch 2025-08-01 13:14:59 -04:00 committed by GitHub
parent ae55eed98f
commit b97fe5e300
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 113 additions and 49 deletions

View File

@ -1,3 +1,7 @@
# Shell text coloring
# https://github.com/tartley/colorama/blob/master/CHANGELOG.rst
colorama
# The Python web framework on which NetBox is built # The Python web framework on which NetBox is built
# https://docs.djangoproject.com/en/stable/releases/ # https://docs.djangoproject.com/en/stable/releases/
Django==5.2.* Django==5.2.*

View File

@ -1,29 +1,48 @@
import code import code
import platform import platform
import sys from collections import defaultdict
from types import SimpleNamespace
from colorama import Fore, Style
from django import get_version from django import get_version
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils.module_loading import import_string
from core.models import ObjectType from netbox.constants import CORE_APPS
from users.models import User from netbox.plugins.utils import get_installed_plugins
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
EXCLUDE_MODELS = (
'extras.branch',
'extras.stagedchange',
)
BANNER_TEXT = """### NetBox interactive shell ({node}) def color(color: str, text: str):
### Python {python} | Django {django} | NetBox {netbox} return getattr(Fore, color.upper()) + text + Style.RESET_ALL
### lsmodels() will show available models. Use help(<model>) for more info.""".format(
node=platform.node(),
python=platform.python_version(), def bright(text: str):
django=get_version(), return Style.BRIGHT + text + Style.RESET_ALL
netbox=settings.RELEASE.name
)
def get_models(app_config):
"""
Return a list of all non-private models within an app.
"""
return [
model for model in app_config.get_models()
if not getattr(model, '_netbox_private', False)
]
def get_constants(app_config):
"""
Return a dictionary mapping of all constants defined within an app.
"""
try:
constants = import_string(f'{app_config.name}.constants')
except ImportError:
return {}
return {
name: value for name, value in vars(constants).items()
}
class Command(BaseCommand): class Command(BaseCommand):
@ -36,47 +55,88 @@ class Command(BaseCommand):
help='Python code to execute (instead of starting an interactive shell)', help='Python code to execute (instead of starting an interactive shell)',
) )
def _lsmodels(self): def _lsapps(self):
for app, models in self.django_models.items(): for app_label in self.django_models.keys():
app_name = apps.get_app_config(app).verbose_name app_name = apps.get_app_config(app_label).verbose_name
print(f'{app_label} - {app_name}')
def _lsmodels(self, app_label=None):
"""
Return a list of all models within each app.
Args:
app_label: The name of a specific app
"""
if app_label:
if app_label not in self.django_models:
print(f"No models listed for {app_label}")
return
app_labels = [app_label]
else:
app_labels = self.django_models.keys() # All apps
for app_label in app_labels:
app_name = apps.get_app_config(app_label).verbose_name
print(f'{app_name}:') print(f'{app_name}:')
for m in models: for m in self.django_models[app_label]:
print(f' {m}') print(f' {m}')
def get_namespace(self): def get_namespace(self):
namespace = {} namespace = defaultdict(SimpleNamespace)
# Gather Django models and constants from each app # Iterate through all core apps & plugins to compile namespace of models and constants
for app in APPS: for app_name in [*CORE_APPS, *get_installed_plugins().keys()]:
models = [] app_config = apps.get_app_config(app_name)
# Load models from each app # Populate models
for model in apps.get_app_config(app).get_models(): if models := get_models(app_config):
app_label = model._meta.app_label for model in models:
model_name = model._meta.model_name setattr(namespace[app_name], model.__name__, model)
if f'{app_label}.{model_name}' not in EXCLUDE_MODELS: self.django_models[app_name] = sorted([
namespace[model.__name__] = model model.__name__ for model in models
models.append(model.__name__) ])
self.django_models[app] = sorted(models)
# Constants # Populate constants
try: for const_name, const_value in get_constants(app_config).items():
app_constants = sys.modules[f'{app}.constants'] setattr(namespace[app_name], const_name, const_value)
for name in dir(app_constants):
namespace[name] = getattr(app_constants, name)
except KeyError:
pass
# Additional objects to include return {
namespace['ObjectType'] = ObjectType **namespace,
namespace['User'] = User 'lsapps': self._lsapps,
# Load convenience commands
namespace.update({
'lsmodels': self._lsmodels, 'lsmodels': self._lsmodels,
}) }
return namespace @staticmethod
def get_banner_text():
lines = [
'{title} ({hostname})'.format(
title=bright('NetBox interactive shell'),
hostname=platform.node(),
),
'{python} | {django} | {netbox}'.format(
python=color('green', f'Python v{platform.python_version()}'),
django=color('green', f'Django v{get_version()}'),
netbox=color('green', settings.RELEASE.name),
),
]
if installed_plugins := get_installed_plugins():
plugin_list = ', '.join([
color('cyan', f'{name} v{version}') for name, version in installed_plugins.items()
])
lines.append(
'Plugins: {plugin_list}'.format(
plugin_list=plugin_list
)
)
lines.append(
'lsapps() & lsmodels() will show available models. Use help(<model>) for more info.'
)
return '\n'.join([
f'### {line}' for line in lines
])
def handle(self, **options): def handle(self, **options):
namespace = self.get_namespace() namespace = self.get_namespace()
@ -97,5 +157,4 @@ class Command(BaseCommand):
readline.parse_and_bind('tab: complete') readline.parse_and_bind('tab: complete')
# Run interactive shell # Run interactive shell
shell = code.interact(banner=BANNER_TEXT, local=namespace) return code.interact(banner=self.get_banner_text(), local=namespace)
return shell

View File

@ -1,3 +1,4 @@
colorama==0.4.6
Django==5.2.4 Django==5.2.4
django-cors-headers==4.7.0 django-cors-headers==4.7.0
django-debug-toolbar==5.2.0 django-debug-toolbar==5.2.0