2022-04-10 19:39:31 +00:00
|
|
|
from datetime import datetime
|
2020-09-06 04:13:42 +00:00
|
|
|
import logging
|
2022-01-15 00:23:50 +00:00
|
|
|
import re
|
2020-09-06 04:13:42 +00:00
|
|
|
|
2022-04-14 02:10:25 +00:00
|
|
|
from app.classes.controllers.crafty_perms_controller import EnumPermissionsCrafty
|
|
|
|
from app.classes.controllers.server_perms_controller import EnumPermissionsServer
|
2021-03-22 04:02:18 +00:00
|
|
|
from app.classes.web.base_handler import BaseHandler
|
2022-06-02 13:40:43 +00:00
|
|
|
from app.classes.models.management import DatabaseShortcuts
|
2020-09-06 04:13:42 +00:00
|
|
|
|
2022-03-08 04:40:44 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
2022-03-23 02:50:12 +00:00
|
|
|
bearer_pattern = re.compile(r"^Bearer", flags=re.IGNORECASE)
|
2020-09-06 04:13:42 +00:00
|
|
|
|
2022-01-26 01:45:30 +00:00
|
|
|
|
2022-03-23 02:50:12 +00:00
|
|
|
class ApiHandler(BaseHandler):
|
2021-04-17 15:19:19 +00:00
|
|
|
def return_response(self, status: int, data: dict):
|
2022-01-26 01:45:30 +00:00
|
|
|
# Define a standardized response
|
2021-04-17 15:19:19 +00:00
|
|
|
self.set_status(status)
|
2020-09-06 04:13:42 +00:00
|
|
|
self.write(data)
|
2022-01-15 00:23:50 +00:00
|
|
|
|
2022-04-10 19:39:31 +00:00
|
|
|
def check_xsrf_cookie(self):
|
|
|
|
# Disable CSRF protection on API routes
|
|
|
|
pass
|
|
|
|
|
2022-03-23 02:50:12 +00:00
|
|
|
def access_denied(self, user, reason=""):
|
2022-01-26 01:45:30 +00:00
|
|
|
if reason:
|
2022-03-23 02:50:12 +00:00
|
|
|
reason = " because " + reason
|
|
|
|
logger.info(
|
|
|
|
"User %s from IP %s was denied access to the API route "
|
|
|
|
+ self.request.path
|
|
|
|
+ reason,
|
|
|
|
user,
|
|
|
|
self.get_remote_ip(),
|
|
|
|
)
|
|
|
|
self.finish(
|
|
|
|
self.return_response(
|
|
|
|
403,
|
|
|
|
{
|
|
|
|
"error": "ACCESS_DENIED",
|
|
|
|
"info": "You were denied access to the requested resource",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
)
|
2022-01-26 01:45:30 +00:00
|
|
|
|
2021-04-17 15:19:19 +00:00
|
|
|
def authenticate_user(self) -> bool:
|
2022-04-10 19:39:31 +00:00
|
|
|
self.permissions = {
|
2022-04-14 02:10:25 +00:00
|
|
|
"Commands": EnumPermissionsServer.COMMANDS,
|
|
|
|
"Terminal": EnumPermissionsServer.TERMINAL,
|
|
|
|
"Logs": EnumPermissionsServer.LOGS,
|
|
|
|
"Schedule": EnumPermissionsServer.SCHEDULE,
|
|
|
|
"Backup": EnumPermissionsServer.BACKUP,
|
|
|
|
"Files": EnumPermissionsServer.FILES,
|
|
|
|
"Config": EnumPermissionsServer.CONFIG,
|
|
|
|
"Players": EnumPermissionsServer.PLAYERS,
|
|
|
|
"Server_Creation": EnumPermissionsCrafty.SERVER_CREATION,
|
|
|
|
"User_Config": EnumPermissionsCrafty.USER_CONFIG,
|
|
|
|
"Roles_Config": EnumPermissionsCrafty.ROLES_CONFIG,
|
2022-04-10 19:39:31 +00:00
|
|
|
}
|
2020-09-06 04:13:42 +00:00
|
|
|
try:
|
2022-03-08 04:40:44 +00:00
|
|
|
logger.debug("Searching for specified token")
|
2022-01-15 00:23:50 +00:00
|
|
|
|
2022-03-23 02:50:12 +00:00
|
|
|
api_token = self.get_argument("token", "")
|
2022-04-10 19:39:31 +00:00
|
|
|
self.api_token = api_token
|
2022-03-23 02:50:12 +00:00
|
|
|
if api_token is None and self.request.headers.get("Authorization"):
|
|
|
|
api_token = bearer_pattern.sub(
|
|
|
|
"", self.request.headers.get("Authorization")
|
|
|
|
)
|
2022-01-15 00:23:50 +00:00
|
|
|
elif api_token is None:
|
2022-03-23 02:50:12 +00:00
|
|
|
api_token = self.get_cookie("token")
|
2022-01-15 00:23:50 +00:00
|
|
|
user_data = self.controller.users.get_user_by_api_token(api_token)
|
|
|
|
|
2022-03-08 04:40:44 +00:00
|
|
|
logger.debug("Checking results")
|
2020-09-06 04:13:42 +00:00
|
|
|
if user_data:
|
2020-09-06 04:58:17 +00:00
|
|
|
# Login successful! Check perms
|
2022-03-08 04:40:44 +00:00
|
|
|
logger.info(f"User {user_data['username']} has authenticated to API")
|
2021-04-17 15:19:19 +00:00
|
|
|
|
2022-03-23 02:50:12 +00:00
|
|
|
return True # This is to set the "authenticated"
|
2022-06-14 12:40:57 +00:00
|
|
|
logging.debug("Auth unsuccessful")
|
|
|
|
self.access_denied("unknown", "the user provided an invalid token")
|
|
|
|
return False
|
2021-04-17 15:19:19 +00:00
|
|
|
except Exception as e:
|
2022-03-08 04:40:44 +00:00
|
|
|
logger.warning("An error occured while authenticating an API user: %s", e)
|
2022-03-23 02:50:12 +00:00
|
|
|
self.finish(
|
|
|
|
self.return_response(
|
|
|
|
403,
|
|
|
|
{
|
|
|
|
"error": "ACCESS_DENIED",
|
|
|
|
"info": "An error occured while authenticating the user",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
)
|
2022-01-15 00:23:50 +00:00
|
|
|
return False
|
2020-09-06 04:13:42 +00:00
|
|
|
|
2020-09-06 04:58:17 +00:00
|
|
|
|
2021-03-22 04:02:18 +00:00
|
|
|
class ServersStats(ApiHandler):
|
2020-09-06 04:58:17 +00:00
|
|
|
def get(self):
|
|
|
|
"""Get details about all servers"""
|
2021-04-17 15:19:19 +00:00
|
|
|
authenticated = self.authenticate_user()
|
2022-04-10 21:30:48 +00:00
|
|
|
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
2022-01-26 01:45:30 +00:00
|
|
|
if not authenticated:
|
|
|
|
return
|
2022-04-10 21:30:48 +00:00
|
|
|
if user_obj["superuser"]:
|
|
|
|
raw_stats = self.controller.servers.get_all_servers_stats()
|
|
|
|
else:
|
|
|
|
raw_stats = self.controller.servers.get_authorized_servers_stats(
|
|
|
|
user_obj["user_id"]
|
|
|
|
)
|
2022-04-10 19:39:31 +00:00
|
|
|
stats = []
|
|
|
|
for rs in raw_stats:
|
|
|
|
s = {}
|
|
|
|
for k, v in rs["server_data"].items():
|
|
|
|
if isinstance(v, datetime):
|
|
|
|
s[k] = v.timestamp()
|
|
|
|
else:
|
|
|
|
s[k] = v
|
|
|
|
stats.append(s)
|
2022-01-26 01:45:30 +00:00
|
|
|
|
2020-09-06 04:58:17 +00:00
|
|
|
# Get server stats
|
2021-04-17 15:19:19 +00:00
|
|
|
# TODO Check perms
|
2022-04-10 19:39:31 +00:00
|
|
|
self.finish(self.write({"servers": stats}))
|
2020-09-06 04:58:17 +00:00
|
|
|
|
|
|
|
|
2021-03-22 04:02:18 +00:00
|
|
|
class NodeStats(ApiHandler):
|
2020-09-06 04:58:17 +00:00
|
|
|
def get(self):
|
|
|
|
"""Get stats for particular node"""
|
2021-04-17 15:19:19 +00:00
|
|
|
authenticated = self.authenticate_user()
|
2022-01-26 01:45:30 +00:00
|
|
|
if not authenticated:
|
|
|
|
return
|
|
|
|
|
2020-09-06 04:58:17 +00:00
|
|
|
# Get node stats
|
2022-05-26 12:50:20 +00:00
|
|
|
node_stats = self.controller.servers.stats.get_node_stats()
|
2022-04-10 19:39:31 +00:00
|
|
|
self.return_response(200, {"code": node_stats["node_stats"]})
|
|
|
|
|
|
|
|
|
|
|
|
class SendCommand(ApiHandler):
|
|
|
|
def post(self):
|
|
|
|
user = self.authenticate_user()
|
|
|
|
|
2022-04-13 01:52:40 +00:00
|
|
|
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
|
|
|
|
2022-04-10 19:39:31 +00:00
|
|
|
if user is None:
|
|
|
|
self.access_denied("unknown")
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
server_id = self.get_argument("id")
|
|
|
|
|
2022-04-13 01:52:40 +00:00
|
|
|
if (
|
|
|
|
not user_obj["user_id"]
|
|
|
|
in self.controller.server_perms.get_server_user_list(server_id)
|
|
|
|
and not user_obj["superuser"]
|
|
|
|
):
|
|
|
|
self.access_denied("unknown")
|
|
|
|
return
|
|
|
|
|
2022-04-10 19:39:31 +00:00
|
|
|
if not self.permissions[
|
|
|
|
"Commands"
|
|
|
|
] in self.controller.server_perms.get_api_key_permissions_list(
|
|
|
|
self.controller.users.get_api_key_by_token(self.api_token), server_id
|
|
|
|
):
|
|
|
|
self.access_denied(user)
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
command = self.get_argument("command", default=None, strip=True)
|
|
|
|
server_id = self.get_argument("id")
|
|
|
|
if command:
|
2022-05-30 17:28:39 +00:00
|
|
|
server = self.controller.servers.get_server_instance_by_id(server_id)
|
2022-04-10 19:39:31 +00:00
|
|
|
if server.check_running:
|
|
|
|
server.send_command(command)
|
|
|
|
self.return_response(200, {"run": True})
|
|
|
|
else:
|
|
|
|
self.return_response(200, {"error": "SER_NOT_RUNNING"})
|
|
|
|
else:
|
|
|
|
self.return_response(200, {"error": "NO_COMMAND"})
|
|
|
|
|
|
|
|
|
|
|
|
class ServerBackup(ApiHandler):
|
|
|
|
def post(self):
|
|
|
|
user = self.authenticate_user()
|
|
|
|
|
2022-04-13 01:52:40 +00:00
|
|
|
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
|
|
|
|
2022-04-10 19:39:31 +00:00
|
|
|
if user is None:
|
|
|
|
self.access_denied("unknown")
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
server_id = self.get_argument("id")
|
|
|
|
|
2022-04-13 01:52:40 +00:00
|
|
|
if (
|
|
|
|
not user_obj["user_id"]
|
|
|
|
in self.controller.server_perms.get_server_user_list(server_id)
|
|
|
|
and not user_obj["superuser"]
|
|
|
|
):
|
|
|
|
self.access_denied("unknown")
|
|
|
|
return
|
|
|
|
|
2022-04-10 19:39:31 +00:00
|
|
|
if not self.permissions[
|
|
|
|
"Backup"
|
|
|
|
] in self.controller.server_perms.get_api_key_permissions_list(
|
|
|
|
self.controller.users.get_api_key_by_token(self.api_token), server_id
|
|
|
|
):
|
|
|
|
self.access_denied(user)
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
|
2022-05-30 17:28:39 +00:00
|
|
|
server = self.controller.servers.get_server_instance_by_id(server_id)
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
server.backup_server()
|
|
|
|
|
|
|
|
self.return_response(200, {"code": "SER_BAK_CALLED"})
|
|
|
|
|
|
|
|
|
|
|
|
class StartServer(ApiHandler):
|
|
|
|
def post(self):
|
|
|
|
user = self.authenticate_user()
|
|
|
|
remote_ip = self.get_remote_ip()
|
|
|
|
|
|
|
|
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
|
|
|
|
|
|
|
if user is None:
|
|
|
|
self.access_denied("unknown")
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
server_id = self.get_argument("id")
|
|
|
|
|
2022-04-13 01:52:40 +00:00
|
|
|
if (
|
|
|
|
not user_obj["user_id"]
|
|
|
|
in self.controller.server_perms.get_server_user_list(server_id)
|
|
|
|
and not user_obj["superuser"]
|
|
|
|
):
|
|
|
|
self.access_denied("unknown")
|
|
|
|
return
|
2022-06-14 12:40:57 +00:00
|
|
|
if not self.permissions[
|
2022-04-10 19:39:31 +00:00
|
|
|
"Commands"
|
|
|
|
] in self.controller.server_perms.get_api_key_permissions_list(
|
|
|
|
self.controller.users.get_api_key_by_token(self.api_token), server_id
|
|
|
|
):
|
2022-04-13 01:52:40 +00:00
|
|
|
self.access_denied("unknown")
|
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
|
2022-05-30 17:28:39 +00:00
|
|
|
server = self.controller.servers.get_server_instance_by_id(server_id)
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
if not server.check_running():
|
|
|
|
self.controller.management.send_command(
|
|
|
|
user_obj["user_id"], server_id, remote_ip, "start_server"
|
|
|
|
)
|
|
|
|
self.return_response(200, {"code": "SER_START_CALLED"})
|
|
|
|
else:
|
|
|
|
self.return_response(500, {"error": "SER_RUNNING"})
|
|
|
|
|
|
|
|
|
|
|
|
class StopServer(ApiHandler):
|
|
|
|
def post(self):
|
|
|
|
user = self.authenticate_user()
|
|
|
|
remote_ip = self.get_remote_ip()
|
|
|
|
|
2022-04-13 01:52:40 +00:00
|
|
|
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
|
|
|
|
2022-04-10 19:39:31 +00:00
|
|
|
if user is None:
|
|
|
|
self.access_denied("unknown")
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
server_id = self.get_argument("id")
|
|
|
|
|
2022-04-13 01:52:40 +00:00
|
|
|
if (
|
|
|
|
not user_obj["user_id"]
|
|
|
|
in self.controller.server_perms.get_server_user_list(server_id)
|
|
|
|
and not user_obj["superuser"]
|
|
|
|
):
|
|
|
|
self.access_denied("unknown")
|
|
|
|
|
2022-04-10 19:39:31 +00:00
|
|
|
if not self.permissions[
|
|
|
|
"Commands"
|
|
|
|
] in self.controller.server_perms.get_api_key_permissions_list(
|
|
|
|
self.controller.users.get_api_key_by_token(self.api_token), server_id
|
|
|
|
):
|
|
|
|
self.access_denied(user)
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
|
2022-05-30 17:28:39 +00:00
|
|
|
server = self.controller.servers.get_server_instance_by_id(server_id)
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
if server.check_running():
|
|
|
|
self.controller.management.send_command(
|
|
|
|
user, server_id, remote_ip, "stop_server"
|
|
|
|
)
|
|
|
|
|
|
|
|
self.return_response(200, {"code": "SER_STOP_CALLED"})
|
|
|
|
else:
|
|
|
|
self.return_response(500, {"error": "SER_NOT_RUNNING"})
|
|
|
|
|
|
|
|
|
|
|
|
class RestartServer(ApiHandler):
|
|
|
|
def post(self):
|
|
|
|
user = self.authenticate_user()
|
|
|
|
remote_ip = self.get_remote_ip()
|
2022-04-13 01:52:40 +00:00
|
|
|
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
if user is None:
|
|
|
|
self.access_denied("unknown")
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
|
|
|
server_id = self.get_argument("id")
|
|
|
|
|
|
|
|
if not user_obj["user_id"] in self.controller.server_perms.get_server_user_list(
|
|
|
|
server_id
|
|
|
|
):
|
|
|
|
self.access_denied("unknown")
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
if not self.permissions[
|
|
|
|
"Commands"
|
|
|
|
] in self.controller.server_perms.get_api_key_permissions_list(
|
|
|
|
self.controller.users.get_api_key_by_token(self.api_token), server_id
|
|
|
|
):
|
|
|
|
self.access_denied(user)
|
|
|
|
|
|
|
|
self.controller.management.send_command(
|
|
|
|
user, server_id, remote_ip, "restart_server"
|
|
|
|
)
|
|
|
|
self.return_response(200, {"code": "SER_RESTART_CALLED"})
|
|
|
|
|
|
|
|
|
|
|
|
class CreateUser(ApiHandler):
|
|
|
|
def post(self):
|
|
|
|
user = self.authenticate_user()
|
2022-04-13 01:52:40 +00:00
|
|
|
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
|
|
|
|
|
|
|
user_perms = self.controller.crafty_perms.get_crafty_permissions_list(
|
|
|
|
user_obj["user_id"]
|
|
|
|
)
|
|
|
|
if (
|
|
|
|
not self.permissions["User_Config"] in user_perms
|
|
|
|
and not user_obj["superuser"]
|
|
|
|
):
|
|
|
|
self.access_denied("unknown")
|
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
if user is None:
|
|
|
|
self.access_denied("unknown")
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
if not self.permissions[
|
|
|
|
"User_Config"
|
|
|
|
] in self.controller.crafty_perms.get_api_key_permissions_list(
|
|
|
|
self.controller.users.get_api_key_by_token(self.api_token)
|
|
|
|
):
|
|
|
|
self.access_denied(user)
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
new_username = self.get_argument("username")
|
|
|
|
new_pass = self.get_argument("password")
|
|
|
|
|
|
|
|
if new_username:
|
|
|
|
self.controller.users.add_user(
|
|
|
|
new_username, new_pass, "default@example.com", True, False
|
|
|
|
)
|
|
|
|
|
|
|
|
self.return_response(
|
|
|
|
200,
|
|
|
|
{
|
|
|
|
"code": "COMPLETE",
|
|
|
|
"username": new_username,
|
|
|
|
"password": new_pass,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
self.return_response(
|
|
|
|
500,
|
|
|
|
{
|
|
|
|
"error": "MISSING_PARAMS",
|
|
|
|
"info": "Some paramaters failed validation",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class DeleteUser(ApiHandler):
|
|
|
|
def post(self):
|
|
|
|
user = self.authenticate_user()
|
|
|
|
|
2022-04-13 01:52:40 +00:00
|
|
|
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
|
|
|
|
|
|
|
user_perms = self.controller.crafty_perms.get_crafty_permissions_list(
|
|
|
|
user_obj["user_id"]
|
|
|
|
)
|
|
|
|
|
|
|
|
if (
|
|
|
|
not self.permissions["User_Config"] in user_perms
|
|
|
|
and not user_obj["superuser"]
|
|
|
|
):
|
|
|
|
self.access_denied("unknown")
|
|
|
|
return
|
|
|
|
|
2022-04-10 19:39:31 +00:00
|
|
|
if user is None:
|
|
|
|
self.access_denied("unknown")
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
if not self.permissions[
|
|
|
|
"User_Config"
|
|
|
|
] in self.controller.crafty_perms.get_api_key_permissions_list(
|
|
|
|
self.controller.users.get_api_key_by_token(self.api_token)
|
|
|
|
):
|
|
|
|
self.access_denied(user)
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
user_id = self.get_argument("user_id", None, True)
|
|
|
|
user_to_del = self.controller.users.get_user_by_id(user_id)
|
|
|
|
|
|
|
|
if user_to_del["superuser"]:
|
|
|
|
self.return_response(
|
|
|
|
500,
|
|
|
|
{"error": "NOT_ALLOWED", "info": "You cannot delete a super user"},
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
if user_id:
|
|
|
|
self.controller.users.remove_user(user_id)
|
|
|
|
self.return_response(200, {"code": "COMPLETED"})
|
|
|
|
|
|
|
|
|
|
|
|
class ListServers(ApiHandler):
|
|
|
|
def get(self):
|
|
|
|
user = self.authenticate_user()
|
|
|
|
user_obj = self.controller.users.get_user_by_api_token(self.api_token)
|
|
|
|
|
|
|
|
if user is None:
|
|
|
|
self.access_denied("unknown")
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
if self.api_token is None:
|
|
|
|
self.access_denied("unknown")
|
2022-04-13 01:52:40 +00:00
|
|
|
return
|
2022-04-10 19:39:31 +00:00
|
|
|
|
|
|
|
if user_obj["superuser"]:
|
|
|
|
servers = self.controller.servers.get_all_defined_servers()
|
|
|
|
servers = [str(i) for i in servers]
|
|
|
|
else:
|
2022-04-13 01:52:40 +00:00
|
|
|
servers = self.controller.servers.get_authorized_servers(
|
|
|
|
user_obj["user_id"]
|
|
|
|
)
|
2022-06-02 13:40:43 +00:00
|
|
|
page_servers = []
|
|
|
|
for server in servers:
|
|
|
|
if server not in page_servers:
|
|
|
|
page_servers.append(
|
|
|
|
DatabaseShortcuts.get_data_obj(server.server_object)
|
|
|
|
)
|
|
|
|
servers = page_servers
|
2022-04-10 19:39:31 +00:00
|
|
|
servers = [str(i) for i in servers]
|
|
|
|
|
|
|
|
self.return_response(
|
|
|
|
200,
|
|
|
|
{
|
|
|
|
"code": "COMPLETED",
|
|
|
|
"servers": servers,
|
|
|
|
},
|
|
|
|
)
|