Merge pull request !2954 from rhodecode-enterprise-ce feature/update_via_ui
Changes from branch: Feature/update via ui
This commit is contained in:
commit
144e4a8ab1
5 changed files with 228 additions and 0 deletions
|
|
@ -209,6 +209,15 @@ def admin_routes(config):
|
||||||
renderer="rhodecode:templates/admin/settings/settings_system_update.mako",
|
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_route(name="admin_settings_exception_tracker", pattern="/settings/exceptions")
|
||||||
config.add_view(
|
config.add_view(
|
||||||
ExceptionsTrackerView,
|
ExceptionsTrackerView,
|
||||||
|
|
|
||||||
38
rhodecode/apps/admin/update_service.py
Normal file
38
rhodecode/apps/admin/update_service.py
Normal file
|
|
@ -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()
|
||||||
|
|
@ -22,9 +22,12 @@ import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
import rhodecode
|
import rhodecode
|
||||||
from rhodecode.apps._base import BaseAppView
|
from rhodecode.apps._base import BaseAppView
|
||||||
from rhodecode.apps._base.navigation import navigation_list
|
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 import helpers as h
|
||||||
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
|
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
|
||||||
from rhodecode.lib.utils2 import str2bool
|
from rhodecode.lib.utils2 import str2bool
|
||||||
|
|
@ -112,6 +115,22 @@ class AdminSystemInfoSettingsView(BaseAppView):
|
||||||
update_state = (
|
update_state = (
|
||||||
{"type": "warning", "message": "New version available: {}".format(version)} if is_outdated else {}
|
{"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 = '<br/> <span class="link" id="rcstack_update" >%s.</span>' % (
|
||||||
|
_("Update Rhodecode to latest version")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
update_literal = "<br/> <span>%s.</span>" % (
|
||||||
|
_("Update in progress, please wait. You can check logs on the host machine")
|
||||||
|
)
|
||||||
|
|
||||||
c.data_items = [
|
c.data_items = [
|
||||||
# update info
|
# update info
|
||||||
(
|
(
|
||||||
|
|
@ -119,6 +138,7 @@ class AdminSystemInfoSettingsView(BaseAppView):
|
||||||
h.literal(
|
h.literal(
|
||||||
'<span class="link" id="check_for_update" >%s.</span>' % (_("Check for updates"))
|
'<span class="link" id="check_for_update" >%s.</span>' % (_("Check for updates"))
|
||||||
+ "<br/> <span >%s.</span>" % (update_info_msg)
|
+ "<br/> <span >%s.</span>" % (update_info_msg)
|
||||||
|
+ update_literal
|
||||||
),
|
),
|
||||||
"",
|
"",
|
||||||
),
|
),
|
||||||
|
|
@ -206,6 +226,22 @@ class AdminSystemInfoSettingsView(BaseAppView):
|
||||||
h.flash("You are not allowed to do this", category="warning")
|
h.flash("You are not allowed to do this", category="warning")
|
||||||
return self._get_template_context(c)
|
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()
|
@LoginRequired()
|
||||||
@HasPermissionAllDecorator("hg.admin")
|
@HasPermissionAllDecorator("hg.admin")
|
||||||
def settings_system_info_check_update(self):
|
def settings_system_info_check_update(self):
|
||||||
|
|
|
||||||
|
|
@ -100,4 +100,7 @@
|
||||||
$('#update_notice').show();
|
$('#update_notice').show();
|
||||||
$('#update_notice').load("${h.route_path('admin_settings_system_update', _query={'ver': request.GET.get('ver')})}");
|
$('#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')}";
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
142
update_server.py
Normal file
142
update_server.py
Normal file
|
|
@ -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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue