mirror of
https://github.com/dkmstr/openuds.git
synced 2025-01-03 01:17:56 +03:00
Added support for importing unmanaged servers using CSV
This commit is contained in:
parent
628f43a2e7
commit
eb86784c62
@ -32,6 +32,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext
|
from django.utils.translation import gettext
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@ -108,7 +109,7 @@ class ServersTokens(ModelHandler):
|
|||||||
|
|
||||||
# REST API For servers (except tunnel servers nor actors)
|
# REST API For servers (except tunnel servers nor actors)
|
||||||
class ServersServers(DetailHandler):
|
class ServersServers(DetailHandler):
|
||||||
custom_methods = ['maintenance']
|
custom_methods = ['maintenance', 'importcsv']
|
||||||
|
|
||||||
def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ManyItemsDictType:
|
def get_items(self, parent: 'Model', item: typing.Optional[str]) -> types.rest.ManyItemsDictType:
|
||||||
parent = typing.cast('models.ServerGroup', parent) # We will receive for sure
|
parent = typing.cast('models.ServerGroup', parent) # We will receive for sure
|
||||||
@ -332,6 +333,74 @@ class ServersServers(DetailHandler):
|
|||||||
item.save()
|
item.save()
|
||||||
return 'ok'
|
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):
|
class ServersGroups(ModelHandler):
|
||||||
model = models.ServerGroup
|
model = models.ServerGroup
|
||||||
|
@ -101,6 +101,7 @@ class DetailHandler(BaseModelHandler):
|
|||||||
"""
|
"""
|
||||||
# Parent init not invoked because their methos are not used on detail handlers (only on parent handlers..)
|
# Parent init not invoked because their methos are not used on detail handlers (only on parent handlers..)
|
||||||
self._parent = parent_handler
|
self._parent = parent_handler
|
||||||
|
self._request = parent_handler._request
|
||||||
self._path = path
|
self._path = path
|
||||||
self._params = params
|
self._params = params
|
||||||
self._args = list(args)
|
self._args = list(args)
|
||||||
@ -123,7 +124,7 @@ class DetailHandler(BaseModelHandler):
|
|||||||
return operation(parent)
|
return operation(parent)
|
||||||
return operation(parent, arg)
|
return operation(parent, arg)
|
||||||
|
|
||||||
return None
|
return consts.rest.NOT_FOUND
|
||||||
|
|
||||||
# pylint: disable=too-many-branches,too-many-return-statements
|
# pylint: disable=too-many-branches,too-many-return-statements
|
||||||
def get(self) -> typing.Any:
|
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
|
# if has custom methods, look for if this request matches any of them
|
||||||
r = self._check_is_custom_method(self._args[0], parent)
|
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
|
return r
|
||||||
|
|
||||||
if nArgs == 1:
|
if nArgs == 1:
|
||||||
@ -195,6 +196,12 @@ class DetailHandler(BaseModelHandler):
|
|||||||
|
|
||||||
parent: models.Model = self._kwargs['parent']
|
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)
|
# Create new item unless 1 param received (the id of the item to modify)
|
||||||
item = None
|
item = None
|
||||||
if len(self._args) == 1:
|
if len(self._args) == 1:
|
||||||
|
@ -159,7 +159,11 @@ class MarshallerProcessor(ContentProcessor):
|
|||||||
|
|
||||||
res = self.marshaller.loads(self._request.body.decode('utf8'))
|
res = self.marshaller.loads(self._request.body.decode('utf8'))
|
||||||
logger.debug('Unmarshalled content: %s', res)
|
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:
|
except Exception as e:
|
||||||
logger.exception('parsing %s: %s', self.mime_type, self._request.body.decode('utf8'))
|
logger.exception('parsing %s: %s', self.mime_type, self._request.body.decode('utf8'))
|
||||||
raise ParametersException(str(e))
|
raise ParametersException(str(e))
|
||||||
|
@ -40,3 +40,10 @@ GUI: typing.Final[str] = 'gui'
|
|||||||
LOG: typing.Final[str] = 'log'
|
LOG: typing.Final[str] = 'log'
|
||||||
|
|
||||||
SYSTEM: typing.Final[str] = 'system' # Defined on system class, here for reference
|
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
@ -163,9 +163,11 @@ gettext("Active");
|
|||||||
gettext("Maintenance");
|
gettext("Maintenance");
|
||||||
gettext("Exit maintenance mode");
|
gettext("Exit maintenance mode");
|
||||||
gettext("Enter maintenance mode");
|
gettext("Enter maintenance mode");
|
||||||
|
gettext("Import CSV");
|
||||||
gettext("Exit maintenance mode?");
|
gettext("Exit maintenance mode?");
|
||||||
gettext("Enter maintenance mode?");
|
gettext("Enter maintenance mode?");
|
||||||
gettext("Maintenance mode for");
|
gettext("Maintenance mode for");
|
||||||
|
gettext("Import servers");
|
||||||
gettext("New server");
|
gettext("New server");
|
||||||
gettext("Edit server");
|
gettext("Edit server");
|
||||||
gettext("Remove server from server group");
|
gettext("Remove server from server group");
|
||||||
@ -497,7 +499,7 @@ gettext("New");
|
|||||||
gettext("New");
|
gettext("New");
|
||||||
gettext("Edit");
|
gettext("Edit");
|
||||||
gettext("Permissions");
|
gettext("Permissions");
|
||||||
gettext("Export");
|
gettext("Export CSV");
|
||||||
gettext("Delete");
|
gettext("Delete");
|
||||||
gettext("Filter");
|
gettext("Filter");
|
||||||
gettext("Selected items");
|
gettext("Selected items");
|
||||||
@ -520,6 +522,18 @@ gettext("Users");
|
|||||||
gettext("Groups");
|
gettext("Groups");
|
||||||
gettext("New permission...");
|
gettext("New permission...");
|
||||||
gettext("Ok");
|
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("Remove all");
|
||||||
gettext("Cancel");
|
gettext("Cancel");
|
||||||
gettext("Ok");
|
gettext("Ok");
|
||||||
|
@ -102,6 +102,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</uds-root>
|
</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>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user