Files
peeringdb-peeringdb/peeringdb_server/admin_commandline_tools.py
T

537 lines
16 KiB
Python
Raw Normal View History

2021-10-15 03:25:38 -05:00
"""
Defines CLI wrappers for django commands that should
be executable through the django-admin interface.
Extend the CommandLineToolWrapper class and call the
register_tool decorator to add support for a new django
command to exposed in this manner.
"""
2020-01-08 13:29:58 -06:00
import io
2018-11-08 19:45:21 +00:00
import json
2022-02-08 13:14:27 -06:00
import traceback
2019-02-15 17:46:20 +00:00
2018-11-08 19:45:21 +00:00
import reversion
from dal import autocomplete
from django import forms
2020-07-26 23:36:27 -05:00
from django.conf import settings
2018-11-08 19:45:21 +00:00
from django.core.management import call_command
2021-11-12 11:16:25 -06:00
from django.db import transaction
2021-07-10 10:12:35 -05:00
from reversion.models import Version
from peeringdb_server import maintenance
2019-12-05 16:57:52 +00:00
from peeringdb_server.models import (
COMMANDLINE_TOOLS,
2021-07-10 10:12:35 -05:00
REFTAG_MAP,
2019-12-05 16:57:52 +00:00
CommandLineTool,
Facility,
2021-07-10 10:12:35 -05:00
InternetExchange,
2019-12-05 16:57:52 +00:00
)
2018-11-08 19:45:21 +00:00
2019-12-05 16:57:52 +00:00
2018-11-08 19:45:21 +00:00
def _(m):
return m
TOOL_MAP = {}
def register_tool(cls):
TOOL_MAP[cls.tool] = cls
2021-08-18 08:21:22 -05:00
return cls
2018-11-08 19:45:21 +00:00
def get_tool(tool_id, form):
"""
Arguments:
tool_id (str): tool_id as it exists in COMMANDLINE_TOOLS
form (django.forms.Form): form instance
Returns:
CommandLineToolWrapper instance
"""
t = TOOL_MAP.get(tool_id)
t = t(form)
return t
def get_tool_from_data(data):
"""
Arguments:
data (dict): dict containing form data, at the very least
needs to have a "tool" key containing the tool_id
Returns:
CommandLineToolWrapper instance
"""
tool_id = data.get("tool")
t = TOOL_MAP.get(tool_id)
form = t.Form(data)
form.is_valid()
t = t(form)
return t
2020-07-15 02:07:01 -05:00
class EmptyId:
2018-11-08 19:45:21 +00:00
id = 0
2020-07-15 02:07:01 -05:00
class CommandLineToolWrapper:
2018-11-08 19:45:21 +00:00
tool = None
2019-01-04 10:02:28 +00:00
queue = 0
maintenance = False
2018-11-08 19:45:21 +00:00
class Form(forms.Form):
pass
def __init__(self, form):
self.status = 0
self.result = None
self.args = []
self.kwargs = {}
self.form_instance = form
2021-08-18 08:21:22 -05:00
self.set_arguments(form.cleaned_data) # lgtm[py/init-calls-subclass]
# LGTM Note: as this is the last statement in the __init__
# call this is unproblematic, however this should probably
# still be separated in the future (TODO)
2018-11-08 19:45:21 +00:00
@property
def name(self):
return dict(COMMANDLINE_TOOLS).get(self.tool)
@property
def form(self):
return self.Form()
@property
def description(self):
return self.tool
@property
def pretty_result(self):
if not self.result:
return ""
r = []
for line in self.result.split("\n"):
if line.find("[error]") > -1:
2020-07-15 02:07:01 -05:00
r.append(f'<div class="error">{line}</div>')
2018-11-08 19:45:21 +00:00
elif line.find("[warning]") > -1:
2020-07-15 02:07:01 -05:00
r.append(f'<div class="warning">{line}</div>')
2018-11-08 19:45:21 +00:00
else:
2020-07-15 02:07:01 -05:00
r.append(f'<div class="info">{line}</div>')
2018-11-08 19:45:21 +00:00
return "\n".join(r)
def set_arguments(self, form_data):
pass
2019-02-15 17:46:20 +00:00
def validate(self):
pass
2022-04-12 16:39:19 -04:00
def _run(self, command, commit=False, user=None):
2020-01-08 13:29:58 -06:00
r = io.StringIO()
2019-01-04 10:02:28 +00:00
if self.maintenance and commit:
maintenance.on()
2018-11-08 19:45:21 +00:00
try:
2019-02-15 17:46:20 +00:00
self.validate()
2018-11-08 19:45:21 +00:00
if commit:
2019-12-05 16:57:52 +00:00
call_command(
self.tool, *self.args, commit=True, stdout=r, **self.kwargs
)
2018-11-08 19:45:21 +00:00
else:
call_command(self.tool, *self.args, stdout=r, **self.kwargs)
self.result = r.getvalue()
except Exception as inst:
2022-02-08 13:14:27 -06:00
traceback.print_exc()
2020-07-15 02:07:01 -05:00
self.result = f"[error] {inst}"
2018-11-08 19:45:21 +00:00
self.status = 1
2019-01-04 10:02:28 +00:00
finally:
if self.maintenance and commit:
maintenance.off()
2018-11-08 19:45:21 +00:00
if commit:
2022-04-12 16:39:19 -04:00
if self.queue:
# if command was processed through the queue, update the existing
# command instance
command.description = self.description
command.status = "done"
command.result = self.result
command.save()
else:
# if command was processed in line with the http request it still
# needs to be persisted to the database
CommandLineTool.objects.create(
user=user,
tool=self.tool,
description=self.description,
status="done",
arguments=json.dumps({"args": self.args, "kwargs": self.kwargs}),
result=self.result,
)
2018-11-08 19:45:21 +00:00
return self.result
2021-11-12 11:16:25 -06:00
@transaction.atomic
2019-01-04 10:02:28 +00:00
def run(self, user, commit=False):
if self.queue and commit:
2019-12-05 16:57:52 +00:00
if (
CommandLineTool.objects.filter(tool=self.tool)
.exclude(status="done")
.count()
>= self.queue
):
self.result = "[error] {}".format(
_(
"This command is already waiting / running - please wait for it to finish before executing it again"
)
)
2019-01-04 10:02:28 +00:00
return self.result
2022-02-08 13:14:27 -06:00
self.cmd_instance = CommandLineTool.objects.create(
2019-12-05 16:57:52 +00:00
user=user,
tool=self.tool,
description=self.description,
status="waiting",
arguments=json.dumps({"args": self.args, "kwargs": self.kwargs}),
result="",
)
self.result = "[warn] {}".format(
_(
"This command takes a while to complete and will be queued and ran in the "
"background. No output log can be provided at this point in time. You may "
"review once the command has finished."
)
)
2022-02-08 13:14:27 -06:00
2019-01-04 10:02:28 +00:00
return self.result
else:
with reversion.create_revision():
2022-04-12 16:39:19 -04:00
return self._run(None, commit=commit, user=user)
2019-01-04 10:02:28 +00:00
2022-02-08 13:14:27 -06:00
def download_link(self):
return None
2018-11-08 19:45:21 +00:00
# TOOL: RENUMBER LAN
@register_tool
class ToolRenumberLans(CommandLineToolWrapper):
"""
This tools runs the pdb_renumber_lans command to
2021-10-15 03:25:38 -05:00
Renumber IP Spaces in an Exchange.
2018-11-08 19:45:21 +00:00
"""
tool = "pdb_renumber_lans"
class Form(forms.Form):
exchange = forms.ModelChoiceField(
queryset=InternetExchange.handleref.undeleted().order_by("name"),
2019-12-05 16:57:52 +00:00
widget=autocomplete.ModelSelect2(url="/autocomplete/ix/json"),
)
2018-11-08 19:45:21 +00:00
old_prefix = forms.CharField(
2019-12-05 16:57:52 +00:00
help_text=_(
"Old prefix - renumber all netixlans that fall into this prefix"
)
)
2018-11-08 19:45:21 +00:00
new_prefix = forms.CharField(
2019-12-05 16:57:52 +00:00
help_text=_(
"New prefix - needs to be the same protocol and length as old prefix"
)
)
2018-11-08 19:45:21 +00:00
@property
def description(self):
2021-10-15 03:25:38 -05:00
"""Provide a human readable description of the command that was run."""
try:
return "{}: {} to {}".format(
2019-12-05 16:57:52 +00:00
InternetExchange.objects.get(id=self.args[0]),
self.args[1],
self.args[2],
)
2021-08-18 08:21:22 -05:00
except Exception:
# if a version of this command was run before, we still need to able
# to display a somewhat useful discription, so fall back to this basic
# display
2020-07-15 02:07:01 -05:00
return f"(Legacy) {self.args}"
2018-11-08 19:45:21 +00:00
def set_arguments(self, form_data):
2019-12-05 16:57:52 +00:00
self.args = [
form_data.get("exchange", EmptyId()).id,
form_data.get("old_prefix"),
form_data.get("new_prefix"),
]
2018-11-08 19:45:21 +00:00
@register_tool
class ToolMergeFacilities(CommandLineToolWrapper):
"""
This tool runs the pdb_fac_merge command to
2021-10-15 03:25:38 -05:00
merge two facilities.
2018-11-08 19:45:21 +00:00
"""
tool = "pdb_fac_merge"
class Form(forms.Form):
other = forms.ModelChoiceField(
queryset=Facility.handleref.undeleted().order_by("name"),
widget=autocomplete.ModelSelect2(url="/autocomplete/fac/json"),
2019-12-05 16:57:52 +00:00
help_text=_("Merge this facility - it will be deleted"),
)
2018-11-08 19:45:21 +00:00
target = forms.ModelChoiceField(
queryset=Facility.handleref.undeleted().order_by("name"),
widget=autocomplete.ModelSelect2(url="/autocomplete/fac/json"),
2019-12-05 16:57:52 +00:00
help_text=_("Target facility"),
)
2018-11-08 19:45:21 +00:00
@property
def description(self):
2021-10-15 03:25:38 -05:00
"""Provide a human readable description of the command that was run."""
2018-11-08 19:45:21 +00:00
return "{} into {}".format(
Facility.objects.get(id=self.kwargs["ids"]),
2019-12-05 16:57:52 +00:00
Facility.objects.get(id=self.kwargs["target"]),
)
2018-11-08 19:45:21 +00:00
def set_arguments(self, form_data):
self.kwargs = {
"ids": str(form_data.get("other", EmptyId()).id),
2019-12-05 16:57:52 +00:00
"target": str(form_data.get("target", EmptyId()).id),
2018-11-08 19:45:21 +00:00
}
@register_tool
class ToolMergeFacilitiesUndo(CommandLineToolWrapper):
"""
This tool runs the pdb_fac_merge_undo command to
2021-10-15 03:25:38 -05:00
undo a facility merge.
2018-11-08 19:45:21 +00:00
"""
tool = "pdb_fac_merge_undo"
class Form(forms.Form):
merge = forms.ModelChoiceField(
2019-12-05 16:57:52 +00:00
queryset=CommandLineTool.objects.filter(tool="pdb_fac_merge").order_by(
"-created"
),
2018-11-08 19:45:21 +00:00
widget=autocomplete.ModelSelect2(
2019-12-05 16:57:52 +00:00
url="/autocomplete/admin/clt-history/pdb_fac_merge/"
),
help_text=_("Undo this merge"),
)
2018-11-08 19:45:21 +00:00
@property
def description(self):
2021-10-15 03:25:38 -05:00
"""Provide a human readable description of the command that was run."""
2018-11-08 19:45:21 +00:00
# in order to make a useful description we need to collect the arguments
# from the merge command that was undone
kwargs = json.loads(
2019-12-05 16:57:52 +00:00
CommandLineTool.objects.get(id=self.kwargs["clt"]).arguments
).get("kwargs")
2018-11-08 19:45:21 +00:00
return "Undo: {} into {}".format(
Facility.objects.get(id=kwargs["ids"]),
2019-12-05 16:57:52 +00:00
Facility.objects.get(id=kwargs["target"]),
)
2018-11-08 19:45:21 +00:00
def set_arguments(self, form_data):
self.kwargs = {"clt": form_data.get("merge", EmptyId()).id}
2019-01-04 10:02:28 +00:00
2019-12-05 16:57:52 +00:00
2019-01-04 10:02:28 +00:00
@register_tool
class ToolReset(CommandLineToolWrapper):
tool = "pdb_wipe"
queue = 1
maintenance = True
class Form(forms.Form):
2019-12-05 16:57:52 +00:00
keep_users = forms.BooleanField(
required=False,
help_text=_(
"Don't delete users. Note that superuser accounts are always kept - regardless of this setting."
),
)
load_data = forms.BooleanField(
required=False, initial=True, help_text=_("Load data from peeringdb API")
)
load_data_url = forms.CharField(
required=False, initial="https://www.peeringdb.com/api"
)
2019-01-04 10:02:28 +00:00
@property
def description(self):
return "Reset environment"
def set_arguments(self, form_data):
self.kwargs = form_data
2019-02-15 17:46:20 +00:00
@register_tool
class ToolUndelete(CommandLineToolWrapper):
"""
2021-10-15 03:25:38 -05:00
Allows restoration of an object object and it's child objects.
2019-02-15 17:46:20 +00:00
"""
2019-12-05 16:57:52 +00:00
2019-02-15 17:46:20 +00:00
tool = "pdb_undelete"
# These are the reftags that are currently supported by this
# tool.
2019-12-05 16:57:52 +00:00
supported_reftags = ["ixlan", "fac"]
2019-02-15 17:46:20 +00:00
class Form(forms.Form):
version = forms.ModelChoiceField(
queryset=Version.objects.all().order_by("-revision_id"),
2019-12-05 16:57:52 +00:00
widget=autocomplete.ModelSelect2(url="/autocomplete/admin/deletedversions"),
help_text=_("Restore this object - search by [reftag] [id]"),
)
2019-02-15 17:46:20 +00:00
@property
def description(self):
return "{reftag} {id}".format(**self.kwargs)
def set_arguments(self, form_data):
version = form_data.get("version")
if not version:
return
reftag = version.content_type.model_class().HandleRef.tag
2019-12-05 16:57:52 +00:00
self.kwargs = {
"reftag": reftag,
"id": version.object_id,
"version_id": version.id,
}
2019-02-15 17:46:20 +00:00
def validate(self):
if self.kwargs.get("reftag") not in self.supported_reftags:
2019-12-05 16:57:52 +00:00
raise ValueError(
_(
"Only {} type objects may be restored "
"through this interface at this point"
).format(",".join(self.supported_reftags))
)
obj = REFTAG_MAP[self.kwargs.get("reftag")].objects.get(
id=self.kwargs.get("id")
)
2019-02-15 17:46:20 +00:00
if obj.status != "deleted":
2020-07-15 02:07:01 -05:00
raise ValueError(f"{obj} is not currently marked as deleted")
2020-07-26 23:36:27 -05:00
@register_tool
class ToolIXFIXPMemberImport(CommandLineToolWrapper):
"""
2024-04-15 12:42:10 -03:00
Allows resets for various parts of the IX-F member data import protocol.
And import IX-F member data for a single Ixlan at a time.
2020-07-26 23:36:27 -05:00
"""
tool = "pdb_ixf_ixp_member_import"
queue = 1
class Form(forms.Form):
ix = forms.ModelChoiceField(
queryset=InternetExchange.objects.all(),
widget=autocomplete.ModelSelect2(url="/autocomplete/ix/json"),
help_text=_(
2024-04-15 12:42:10 -03:00
"Select an Internet Exchange to perform an IX-F memberdata import"
2020-07-26 23:36:27 -05:00
),
)
if settings.RELEASE_ENV != "prod":
# reset toggles are not available on production
# environment
reset = forms.BooleanField(
required=False, initial=False, help_text=_("Reset all")
)
reset_hints = forms.BooleanField(
required=False, initial=False, help_text=_("Reset hints")
)
reset_dismisses = forms.BooleanField(
required=False, initial=False, help_text=_("Reset dismisses")
)
reset_email = forms.BooleanField(
required=False, initial=False, help_text=_("Reset email")
)
reset_tickets = forms.BooleanField(
required=False, initial=False, help_text=_("Reset tickets")
)
@property
def description(self):
return "IX-F Member Import Tool"
def set_arguments(self, form_data):
for key in [
"reset",
"reset_hints",
"reset_dismisses",
"reset_email",
"reset_tickets",
]:
self.kwargs[key] = form_data.get(key, False)
if form_data.get("ix"):
self.kwargs["ixlan"] = [form_data.get("ix").id]
2022-02-08 13:14:27 -06:00
@register_tool
class ToolValidateData(CommandLineToolWrapper):
"""
Validate data in the database.
"""
tool = "pdb_validate_data"
queue = 10
class Form(forms.Form):
models = []
for tag, model in REFTAG_MAP.items():
models.append((tag, model._meta.verbose_name.title()))
handleref_tag = forms.ChoiceField(
choices=models,
help_text=_("Select a handleref tag to validate"),
label=_("Object type"),
)
field_name = forms.CharField(
required=True,
help_text=_("Enter a field name to validate"),
)
exclude = forms.ChoiceField(
choices=(
(None, "--"),
("valid", _("Valid")),
("invalid", _("Invalid")),
),
required=False,
help_text=_("Exclude data based on validation result"),
)
verbose = forms.BooleanField(
required=False, initial=False, help_text=_("Verbose output")
)
@property
def description(self):
2022-07-15 21:47:59 +03:00
return f"Validate data: {self.args[0]}.{self.args[1]}"
2022-02-08 13:14:27 -06:00
def set_arguments(self, form_data):
self.args = [
form_data.get("handleref_tag"),
form_data.get("field_name"),
]
self.kwargs = {
"exclude": form_data.get("exclude", None),
"verbose": form_data.get("verbose", False),
}
self.form_data = form_data
def download_link(self):
handleref_tag = self.args[0]
field_name = self.args[1]
return (
f"/pdb_validate_data/export/pdb_validate_data_{handleref_tag}_{field_name}.csv",
"Download validation results (CSV) (This file has an expiry date!)",
)