1
0
mirror of https://github.com/dkmstr/openuds.git synced 2024-12-22 13:34:04 +03:00

Added support for importing unmanaged servers using CSV

This commit is contained in:
Adolfo Gómez García 2024-04-28 18:58:16 +02:00
parent 628f43a2e7
commit eb86784c62
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
8 changed files with 109 additions and 8 deletions

View File

@ -32,6 +32,7 @@
import logging
import typing
from django.db.models import Q
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
@ -108,7 +109,7 @@ class ServersTokens(ModelHandler):
# REST API For servers (except tunnel servers nor actors)
class ServersServers(DetailHandler):
custom_methods = ['maintenance']
custom_methods = ['maintenance', 'importcsv']
def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ManyItemsDictType:
parent = typing.cast('models.ServerGroup', parent) # We will receive for sure
@ -332,6 +333,74 @@ class ServersServers(DetailHandler):
item.save()
return 'ok'
def importcsv(self, parent: 'Model') -> typing.Any:
"""
We receive a json with string[][] format with the data.
Has no header, only the data.
"""
parent = ensure.is_instance(parent, models.ServerGroup)
data: list[list[str]] = self._params.get('data', [])
logger.debug('Data received: %s', data)
# String lines can have 1, 2 or 3 fields.
# if 1, it's a IP
# if 2, it's a IP and a hostname. Hostame can be empty, in this case, it will be the same as IP
# if 3, it's a IP, a hostname and a MAC. MAC can be empty, in this case, it will be UNKNOWN
# if ip is empty and has a hostname, it will be kept, but if it has no hostname, it will be skipped
# If the IP is invalid and has no hostname, it will be skipped
import_errors: list[str] = []
for line_number, row in enumerate(data, 1):
if len(row) == 0:
continue
ip = row[0].strip()
hostname = ip
mac = consts.MAC_UNKNOWN
if len(row) > 1:
hostname = row[1].strip()
if len(row) > 2:
mac = row[2].strip().upper().strip() or consts.MAC_UNKNOWN
if mac and not net.is_valid_mac(mac):
import_errors.append(f'Line {line_number}: MAC {mac} is invalid, skipping')
continue # skip invalid macs
if ip and not net.is_valid_ip(ip):
import_errors.append(f'Line {line_number}: IP {ip} is invalid, skipping')
continue # skip invalid ips if not empty
# Must have at least a valid ip or a valid hostname
if not ip and not hostname:
import_errors.append(f'Line {line_number}: No IP or hostname, skipping')
continue
if hostname != ip and not net.is_valid_host(hostname):
# Log it has been skipped
import_errors.append(f'Line {line_number}: Hostname {hostname} is invalid, skipping')
continue # skip invalid hostnames
# Seems valid, create server if not exists already (by ip OR hostname)
logger.debug('Creating server with ip %s, hostname %s and mac %s', ip, hostname, mac)
try:
if parent.servers.filter(Q(ip=ip) | Q(hostname=hostname)).count() == 0:
server = models.Server.objects.create(
register_username=self._user.name,
register_ip=self._request.ip,
ip=ip,
hostname=hostname,
listen_port=0,
mac=mac,
type=parent.type,
subtype=parent.subtype,
stamp=sql_now(),
)
parent.servers.add(server) # And register it on group
else:
# Log it has been skipped
import_errors.append(
f'Line {line_number}: duplicated server, skipping'
)
except Exception as e:
import_errors.append(f'Error creating server on line {line_number}: {str(e)}')
logger.exception('Error creating server on line %s', line_number)
return import_errors
class ServersGroups(ModelHandler):
model = models.ServerGroup

View File

@ -101,6 +101,7 @@ class DetailHandler(BaseModelHandler):
"""
# Parent init not invoked because their methos are not used on detail handlers (only on parent handlers..)
self._parent = parent_handler
self._request = parent_handler._request
self._path = path
self._params = params
self._args = list(args)
@ -123,7 +124,7 @@ class DetailHandler(BaseModelHandler):
return operation(parent)
return operation(parent, arg)
return None
return consts.rest.NOT_FOUND
# pylint: disable=too-many-branches,too-many-return-statements
def get(self) -> typing.Any:
@ -141,7 +142,7 @@ class DetailHandler(BaseModelHandler):
# if has custom methods, look for if this request matches any of them
r = self._check_is_custom_method(self._args[0], parent)
if r is not None:
if r is not consts.rest.NOT_FOUND:
return r
if nArgs == 1:
@ -195,6 +196,12 @@ class DetailHandler(BaseModelHandler):
parent: models.Model = self._kwargs['parent']
# if has custom methods, look for if this request matches any of them
if len(self._args) > 0:
r = self._check_is_custom_method(self._args[1], parent)
if r is not consts.rest.NOT_FOUND:
return r
# Create new item unless 1 param received (the id of the item to modify)
item = None
if len(self._args) == 1:

View File

@ -159,7 +159,11 @@ class MarshallerProcessor(ContentProcessor):
res = self.marshaller.loads(self._request.body.decode('utf8'))
logger.debug('Unmarshalled content: %s', res)
return res
if not isinstance(res, dict):
raise ParametersException('Invalid content')
return typing.cast(dict[str, typing.Any], res)
except Exception as e:
logger.exception('parsing %s: %s', self.mime_type, self._request.body.decode('utf8'))
raise ParametersException(str(e))

View File

@ -40,3 +40,10 @@ GUI: typing.Final[str] = 'gui'
LOG: typing.Final[str] = 'log'
SYSTEM: typing.Final[str] = 'system' # Defined on system class, here for reference
class _NotFound:
pass
NOT_FOUND: typing.Final[_NotFound] = _NotFound()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -163,9 +163,11 @@ gettext("Active");
gettext("Maintenance");
gettext("Exit maintenance mode");
gettext("Enter maintenance mode");
gettext("Import CSV");
gettext("Exit maintenance mode?");
gettext("Enter maintenance mode?");
gettext("Maintenance mode for");
gettext("Import servers");
gettext("New server");
gettext("Edit server");
gettext("Remove server from server group");
@ -497,7 +499,7 @@ gettext("New");
gettext("New");
gettext("Edit");
gettext("Permissions");
gettext("Export");
gettext("Export CSV");
gettext("Delete");
gettext("Filter");
gettext("Selected items");
@ -520,6 +522,18 @@ gettext("Users");
gettext("Groups");
gettext("New permission...");
gettext("Ok");
gettext("CVS Import options for");
gettext("Header");
gettext("CSV contains header line");
gettext("CSV DOES NOT contains header line");
gettext("Separator");
gettext("Use semicolon");
gettext("Use comma");
gettext("Use pipe");
gettext("Use tab");
gettext("File");
gettext("Ok");
gettext("Cancel");
gettext("Remove all");
gettext("Cancel");
gettext("Ok");

View File

@ -102,6 +102,6 @@
</svg>
</div>
</uds-root>
<script src="/uds/res/admin/runtime.js?stamp=1713376179" type="module"></script><script src="/uds/res/admin/polyfills.js?stamp=1713376179" type="module"></script><script src="/uds/res/admin/main.js?stamp=1713376179" type="module"></script></body>
<script src="/uds/res/admin/runtime.js?stamp=1714323481" type="module"></script><script src="/uds/res/admin/polyfills.js?stamp=1714323481" type="module"></script><script src="/uds/res/admin/main.js?stamp=1714323481" type="module"></script></body>
</html>