diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py
index d43e1077..2e4d7aa2 100644
--- a/rhodecode/apps/admin/__init__.py
+++ b/rhodecode/apps/admin/__init__.py
@@ -209,6 +209,15 @@ def admin_routes(config):
renderer="rhodecode:templates/admin/settings/settings_system_update.mako",
)
+ config.add_route(name="settings_system_info_rcstack_update", pattern="/settings/system/rcstack/update")
+ config.add_view(
+ AdminSystemInfoSettingsView,
+ attr="settings_system_info_rcstack_update",
+ route_name="settings_system_info_rcstack_update",
+ request_method="GET",
+ renderer="rhodecode:templates/admin/settings/settings_system_update.mako",
+ )
+
config.add_route(name="admin_settings_exception_tracker", pattern="/settings/exceptions")
config.add_view(
ExceptionsTrackerView,
diff --git a/rhodecode/apps/admin/update_service.py b/rhodecode/apps/admin/update_service.py
new file mode 100644
index 00000000..8aee9cd0
--- /dev/null
+++ b/rhodecode/apps/admin/update_service.py
@@ -0,0 +1,38 @@
+import logging
+import os
+
+import requests
+
+from rhodecode.lib.type_utils import str2bool
+
+
+class RcStackUpdateService:
+ class State:
+ READY = "ready"
+ PROCESSING = "processing"
+
+ def __init__(self, server_host: str, server_port: int):
+ self.host = server_host
+ self.port = server_port
+ self.log = logging.getLogger(__name__)
+
+ def feature_available(self):
+ if str2bool(os.environ.get("DISABLE_RC_UPDATE_SERVICE", "false")) or self.host is None:
+ return False
+
+ try:
+ self.get_state()
+ return True
+ except Exception as e:
+ self.log.error(f"Server on host machine not available, error: {e}")
+ return False
+
+ def get_state(self):
+ url_base = f"http://{self.host}:{self.port}"
+ url = f"{url_base}/state"
+ return requests.get(url).json().get("state", None)
+
+ def rcstack_update(self):
+ url_base = f"http://{self.host}:{self.port}"
+ url = f"{url_base}/rcstack/update"
+ return requests.get(url).json()
diff --git a/rhodecode/apps/admin/views/system_info.py b/rhodecode/apps/admin/views/system_info.py
index ce75a584..24102b9e 100644
--- a/rhodecode/apps/admin/views/system_info.py
+++ b/rhodecode/apps/admin/views/system_info.py
@@ -22,9 +22,12 @@ import urllib.error
import urllib.parse
import os
+from pyramid.httpexceptions import HTTPFound
+
import rhodecode
from rhodecode.apps._base import BaseAppView
from rhodecode.apps._base.navigation import navigation_list
+from rhodecode.apps.admin.update_service import RcStackUpdateService
from rhodecode.lib import helpers as h
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
from rhodecode.lib.utils2 import str2bool
@@ -112,6 +115,22 @@ class AdminSystemInfoSettingsView(BaseAppView):
update_state = (
{"type": "warning", "message": "New version available: {}".format(version)} if is_outdated else {}
)
+
+ update_service = self._get_rc_update_service()
+ update_feature_available = update_service.feature_available()
+
+ update_literal = ""
+ if update_feature_available:
+ update_service_state = update_service.get_state()
+ if update_service_state == RcStackUpdateService.State.READY:
+ update_literal = '
%s.' % (
+ _("Update Rhodecode to latest version")
+ )
+ else:
+ update_literal = "
%s." % (
+ _("Update in progress, please wait. You can check logs on the host machine")
+ )
+
c.data_items = [
# update info
(
@@ -119,6 +138,7 @@ class AdminSystemInfoSettingsView(BaseAppView):
h.literal(
'%s.' % (_("Check for updates"))
+ "
%s." % (update_info_msg)
+ + update_literal
),
"",
),
@@ -206,6 +226,22 @@ class AdminSystemInfoSettingsView(BaseAppView):
h.flash("You are not allowed to do this", category="warning")
return self._get_template_context(c)
+ @LoginRequired()
+ @HasPermissionAllDecorator("hg.admin")
+ def settings_system_info_rcstack_update(self):
+ update_service = self._get_rc_update_service()
+ update_feature_available = update_service.feature_available()
+
+ if update_feature_available:
+ response = update_service.rcstack_update()
+ log.debug(f"update server response: {response}")
+ return HTTPFound(h.route_path("admin_settings_system"))
+
+ def _get_rc_update_service(self) -> RcStackUpdateService:
+ host = os.environ.get("RC_UPDATE_HOST", None)
+ port = os.environ.get("RC_UPDATE_PORT", 10025)
+ return RcStackUpdateService(server_host=host, server_port=port)
+
@LoginRequired()
@HasPermissionAllDecorator("hg.admin")
def settings_system_info_check_update(self):
diff --git a/rhodecode/templates/admin/settings/settings_system.mako b/rhodecode/templates/admin/settings/settings_system.mako
index edaf81c0..5c359ab0 100644
--- a/rhodecode/templates/admin/settings/settings_system.mako
+++ b/rhodecode/templates/admin/settings/settings_system.mako
@@ -100,4 +100,7 @@
$('#update_notice').show();
$('#update_notice').load("${h.route_path('admin_settings_system_update', _query={'ver': request.GET.get('ver')})}");
})
+ $('#rcstack_update').on('click', function(e){
+ window.location.href = "${h.route_path('settings_system_info_rcstack_update')}";
+ })
diff --git a/update_server.py b/update_server.py
new file mode 100644
index 00000000..7bba27cf
--- /dev/null
+++ b/update_server.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python
+import json
+import os
+import subprocess
+import time
+import threading
+
+try:
+ # Py3
+ from http.server import BaseHTTPRequestHandler, HTTPServer
+except ImportError:
+ # Py2
+ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+
+try:
+ from shlex import quote as shell_quote # Py3
+except ImportError:
+ from pipes import quote as shell_quote # Py2 (Unix)
+
+RCSTACK_PATH_NAME = "RCSTACK_PATH"
+RCSTACK_PATH = os.environ.get(RCSTACK_PATH_NAME)
+RCSTACK_LOG_NAME = os.environ.get("RCSTACK_LOG_NAME", "./rcstack_status.log")
+STATUS = {"state": "ready"} # "ready" or "processing"
+
+
+def _log_line(text, log_path):
+ # compatible with python 2
+ out = open(log_path, "ab")
+ try:
+ if not isinstance(text, bytes):
+ text = text.encode("utf-8")
+ out.write(text + b"\n")
+ out.flush()
+ finally:
+ out.close()
+
+
+def _watch_process(popen_obj, log_path):
+ try:
+ rc = popen_obj.wait()
+ stamp = time.strftime("%Y-%m-%d %H:%M:%S")
+ _log_line("[" + stamp + "] finished rc=" + str(rc), log_path=log_path)
+ except Exception as e:
+ stamp = time.strftime("%Y-%m-%d %H:%M:%S")
+ _log_line("[" + stamp + "] watcher error: " + str(e), log_path=log_path)
+ finally:
+ STATUS["state"] = "ready"
+
+
+class RcstackUpdater(BaseHTTPRequestHandler):
+ def __init__(self, request, client_address, server):
+ BaseHTTPRequestHandler.__init__(self, request, client_address, server)
+
+ def _send_json(self, obj, code=200):
+ body = json.dumps(obj).encode("utf-8")
+ self.send_response(code)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ def do_GET(self):
+ if self.path == "/state":
+ return self._send_json({"state": STATUS["state"]})
+
+ elif self.path == "/rcstack/update":
+ # If already processing, return ok and do not spawn a new process.
+ if STATUS["state"] != "ready":
+ return self._send_json({"status": "ok", "state": STATUS["state"]})
+
+ ok, err_msg = self.spawn_rcstack_status()
+ # If spawn failed for real reasons (bad env/path/etc.), return 500
+ if not ok:
+ return self._send_http_error(err_msg.encode("utf-8"), status_code=500)
+
+ return self._send_json({"status": "ok", "state": STATUS["state"]})
+
+ return self._send_http_error(b"not found", status_code=404)
+
+ def spawn_rcstack_status(self):
+ if not RCSTACK_PATH:
+ return False, "%s env variable not set" % (RCSTACK_PATH_NAME,)
+
+ if not os.path.exists(RCSTACK_PATH) or not os.path.isfile(RCSTACK_PATH):
+ return False, "%s does not exist" % (RCSTACK_PATH,)
+
+ rcstack = os.path.expanduser(RCSTACK_PATH)
+ rcstack_dir = os.path.dirname(rcstack)
+ log_path = os.path.join(rcstack_dir, RCSTACK_LOG_NAME)
+
+ STATUS["state"] = "processing"
+
+ stamp = time.strftime("%Y-%m-%d %H:%M:%S")
+ _log_line("[%s] spawn: %s status" % (stamp, rcstack), log_path=log_path)
+
+ out = open(log_path, "ab")
+ try:
+ cmd = "%s self-update && %s stack-upgrade all" % (shell_quote(rcstack), shell_quote(rcstack))
+
+ p = subprocess.Popen(
+ ["/bin/sh", "-c", cmd],
+ cwd=rcstack_dir,
+ stdout=out,
+ stderr=out,
+ close_fds=True,
+ preexec_fn=os.setsid, # Unix only
+ )
+ except Exception as e:
+ STATUS["state"] = "ready"
+ _log_line("[%s] spawn error: %s" % (stamp, str(e)), log_path=log_path)
+ return False, "spawn failed: %s" % (str(e),)
+ finally:
+ try:
+ out.close()
+ except Exception:
+ pass
+
+ t = threading.Thread(target=_watch_process, args=(p, log_path))
+ try:
+ t.daemon = True
+ except Exception:
+ t.setDaemon(True) # Py2
+ t.start()
+
+ return True, None
+
+ def _send_http_error(self, msg, status_code):
+ self.send_response(status_code)
+ self.send_header("Content-Type", "text/plain")
+ self.end_headers()
+ self.wfile.write(msg)
+
+
+def main(host="127.0.0.1", port=10025):
+ httpd = HTTPServer((host, port), RcstackUpdater)
+ stamp = time.strftime("%Y-%m-%d %H:%M:%S")
+ print("[%s] listening on http://%s:%d" % (stamp, host, port))
+ httpd.serve_forever()
+
+
+if __name__ == "__main__":
+ main()