diff --git a/api/decorators.py b/api/decorators.py index ce4ac71e..2e96d6c7 100644 --- a/api/decorators.py +++ b/api/decorators.py @@ -46,6 +46,7 @@ from django.core.exceptions import PermissionDenied from api.views import View +from api.exceptions import ServiceUnavailable from api.utils.request import is_request from api.dc.utils import get_dc from que.lock import TaskLock @@ -152,6 +153,14 @@ def decorator(func): return decorator +def _check_system_update(request): + """Only SuperAdmins can access the API during system update""" + from api.system.update.api_views import UpdateView + + if UpdateView.is_task_running() and not request.user.is_staff: + raise ServiceUnavailable('System update in progress') + + def request_data(catch_dc=True, force_dc=None, permissions=()): def request_data_decorator(fun): """ @@ -211,6 +220,9 @@ def wrap(request, *args, **kwargs): request.user, request.method, fun.__name__, args, kwargs, perm.__name__, request.dc) raise PermissionDenied + # Only SuperAdmins can access the API during system update + _check_system_update(request) + return fun(request, *args, **kwargs) wrap.__name__ = fun.__name__ diff --git a/api/event.py b/api/event.py index d79598ec..8b9c5b94 100644 --- a/api/event.py +++ b/api/event.py @@ -34,3 +34,17 @@ def __init__(self, user_id, dc_id=None, **kwargs): task_id = task_id_from_string(user_id, dummy=True, dc_id=dc_id, tt=TT_DUMMY, tg=tg) kwargs['direct'] = True super(DirectEvent, self).__init__(task_id, **kwargs) + + +class BroadcastEvent(Event): + """ + Broadcast task event dispatched to socket.io monitor, which then sends the signal to all active users. + """ + def __init__(self, task_id=None, **kwargs): + if not task_id: + dc_id = cq.conf.ERIGONES_DEFAULT_DC # DefaultDc().id + system_user_id = cq.conf.ERIGONES_TASK_USER # 7 + task_id = task_id_from_string(system_user_id, dummy=True, dc_id=dc_id, tt=TT_DUMMY, tg=TG_DC_UNBOUND) + + kwargs['broadcast'] = True + super(BroadcastEvent, self).__init__(task_id, **kwargs) diff --git a/api/exceptions.py b/api/exceptions.py index ff160f13..9af105bc 100644 --- a/api/exceptions.py +++ b/api/exceptions.py @@ -281,6 +281,11 @@ class OperationNotSupported(APIException): default_detail = _('Operation not supported') +class ServiceUnavailable(APIException): + status_code = status.HTTP_503_SERVICE_UNAVAILABLE + default_detail = _('Service Unavailable') + + class GatewayTimeout(APIException): status_code = status.HTTP_504_GATEWAY_TIMEOUT default_detail = _('Gateway Timeout') diff --git a/api/node/sysinfo/tasks.py b/api/node/sysinfo/tasks.py index 80358500..3263d298 100644 --- a/api/node/sysinfo/tasks.py +++ b/api/node/sysinfo/tasks.py @@ -12,9 +12,11 @@ # noinspection PyProtectedMember from api.vm.status.tasks import vm_status_all from api.dns.record.api_views import RecordView +from api.system.node.events import NodeSystemRestarted from vms.models import Node, DefaultDc, IPAddress from vms.signals import node_created, node_json_changed, node_json_unchanged from que.tasks import cq, get_task_logger +from que.utils import TASK_USER, owner_id_from_task_id from que.mgmt import MgmtCallbackTask from que.exceptions import TaskException @@ -164,4 +166,14 @@ def node_sysinfo_cb(result, task_id, node_uuid=None): except Exception as e: logger.exception(e) + # Refresh cached version information + emit event informing about restarted erigonesd:fast + try: + del node.system_version + + if owner_id_from_task_id(task_id) == TASK_USER: # internal user ID + NodeSystemRestarted(node, system_version=node.system_version).send() + + except Exception as e: + logger.exception(e) + return result diff --git a/api/system/node/api_views.py b/api/system/node/api_views.py index 986844e3..f9ba2283 100644 --- a/api/system/node/api_views.py +++ b/api/system/node/api_views.py @@ -4,20 +4,18 @@ from django.utils.six import text_type from api.api_views import APIView -from api.exceptions import (NodeIsNotOperational, PreconditionRequired, TaskIsAlreadyRunning, - ObjectNotFound, GatewayTimeout) +# noinspection PyProtectedMember +from api.fields import get_boolean_value +from api.exceptions import NodeIsNotOperational, PreconditionRequired, ObjectNotFound, GatewayTimeout from api.node.utils import get_node, get_nodes from api.system.messages import LOG_SYSTEM_UPDATE from api.system.node.serializers import NodeVersionSerializer -from api.system.node.events import NodeUpdateStarted, NodeUpdateFinished from api.system.service.control import NodeServiceControl from api.system.update.serializers import UpdateSerializer -from api.system.update.utils import process_update_reply -from api.task.response import SuccessTaskResponse, FailureTaskResponse -from que import TG_DC_UNBOUND, TT_DUMMY, Q_FAST -from que.lock import TaskLock -from que.utils import task_id_from_request, worker_command -from vms.models import DefaultDc +from api.task.response import SuccessTaskResponse, FailureTaskResponse, TaskResponse +from que import Q_FAST, TG_DC_UNBOUND +from que.utils import worker_command +from que.tasks import execute logger = getLogger(__name__) @@ -34,6 +32,9 @@ def __init__(self, request, hostname, data): if hostname: self.node = get_node(request, hostname) + + if self.data and get_boolean_value(self.data.get('fresh', None)): + del self.node.system_version # Remove cached version information else: self.node = get_nodes(request) @@ -70,28 +71,37 @@ def get(self): class NodeUpdateView(APIView): """api.system.node.views.system_node_update""" + LOCK = 'system_node_update:%s' dc_bound = False - _lock_key = 'system_update' def __init__(self, request, hostname, data): super(NodeUpdateView, self).__init__(request) self.hostname = hostname self.data = data self.node = get_node(request, hostname) - self.task_id = task_id_from_request(self.request, dummy=True, tt=TT_DUMMY, tg=TG_DC_UNBOUND) - def _update(self, version, key=None, cert=None): + def _update_v2(self, version, key=None, cert=None): + from api.system.update.utils import process_update_reply + node = self.node worker = node.worker(Q_FAST) - logger.debug('Running node "%s" system update to version: "%s"', node, version) + logger.info('Running oldstyle (v2.x) node "%s" system update to version: "%s"', node, version) reply = worker_command('system_update', worker, version=version, key=key, cert=cert, timeout=600) if reply is None: raise GatewayTimeout('Node worker is not responding') - response_class, result = process_update_reply(reply, node, version) - response = response_class(self.request, result, task_id=self.task_id, obj=node, msg=LOG_SYSTEM_UPDATE, - detail_dict=result, dc_bound=False) + result, error = process_update_reply(reply, node, version) + + if error: + response_class = FailureTaskResponse + else: + response_class = SuccessTaskResponse + + detail_dict = result.copy() + detail_dict['version'] = version + response = response_class(self.request, result, obj=node, msg=LOG_SYSTEM_UPDATE, dc_bound=False, + detail_dict=detail_dict) if response.status_code == 200: # Restart all erigonesd workers @@ -102,49 +112,72 @@ def _update(self, version, key=None, cert=None): return response - @classmethod - def get_task_lock(cls): - # Also used in socket.io namespace - return TaskLock(cls._lock_key, desc='System task') - def put(self): - assert self.request.dc.id == DefaultDc().id + assert self.request.dc.is_default() ser = UpdateSerializer(self.request, data=self.data) if not ser.is_valid(): - return FailureTaskResponse(self.request, ser.errors, task_id=self.task_id, dc_bound=False) + return FailureTaskResponse(self.request, ser.errors, dc_bound=False) node = self.node - version = ser.object['version'] - + version = ser.data['version'] + key = ser.data.get('key') + cert = ser.data.get('cert') + del node.system_version # Request latest version in next command node_version = node.system_version if not (isinstance(node_version, text_type) and node_version): raise NodeIsNotOperational('Node version information could not be retrieved') - if version == ('v' + node.system_version): + node_version = node_version.split(':')[-1] # remove edition prefix + + if version == ('v' + node_version) and not ser.data.get('force'): raise PreconditionRequired('Node is already up-to-date') if node.status != node.OFFLINE: - raise NodeIsNotOperational('Unable to perform update on node that is not in maintenance state!') + raise NodeIsNotOperational('Unable to perform update on node that is not in maintenance state') - lock = self.get_task_lock() + if node_version.startswith('2.'): + # Old-style (pre 3.0) update mechanism + return self._update_v2(version, key=key, cert=cert) - if not lock.acquire(self.task_id, timeout=7200, save_reverse=False): - raise TaskIsAlreadyRunning + # Upload key and cert and get command array + worker = node.worker(Q_FAST) + update_cmd = worker_command('system_update_command', worker, version=version, key=key, cert=cert, + force=ser.data.get('force'), timeout=10) - try: - # Emit event into socket.io - NodeUpdateStarted(self.task_id, request=self.request).send() + if update_cmd is None: + raise GatewayTimeout('Node worker is not responding') - return self._update(version, key=ser.object.get('key'), cert=ser.object.get('cert')) - finally: - lock.delete(fail_silently=True, delete_reverse=False) - # Delete cached node version information (will be cached again during next node.system_version call) - del node.system_version - # Emit event into socket.io - NodeUpdateFinished(self.task_id, request=self.request).send() + if not isinstance(update_cmd, list): + raise PreconditionRequired('Node update command could be retrieved') + + msg = LOG_SYSTEM_UPDATE + _apiview_ = { + 'view': 'system_node_update', + 'method': self.request.method, + 'hostname': node.hostname, + 'version': version, + } + meta = { + 'apiview': _apiview_, + 'msg': msg, + 'node_uuid': node.uuid, + 'output': {'returncode': 'returncode', 'stdout': 'message'}, + 'check_returncode': True, + } + lock = self.LOCK % node.hostname + cmd = '%s 2>&1' % ' '.join(update_cmd) + + tid, err = execute(self.request, node.owner.id, cmd, meta=meta, lock=lock, queue=node.fast_queue, + tg=TG_DC_UNBOUND) + + if err: + return FailureTaskResponse(self.request, err, dc_bound=False) + else: + return TaskResponse(self.request, tid, msg=msg, obj=node, api_view=_apiview_, data=self.data, + dc_bound=False, detail_dict=ser.detail_dict(force_full=True)) class NodeLogsView(APIView): diff --git a/api/system/node/events.py b/api/system/node/events.py index dfd4bbc4..fd4c8e66 100644 --- a/api/system/node/events.py +++ b/api/system/node/events.py @@ -1,15 +1,16 @@ -from api.system.update.events import BaseUpdateEvent +from api.event import Event +from que import TT_DUMMY, TG_DC_UNBOUND +from que.utils import DEFAULT_DC, task_id_from_string -class NodeUpdateStarted(BaseUpdateEvent): +class NodeSystemRestarted(Event): """ - Called from the NodeUpdateView. + Called from node_sysinfo_cb after erigonesd:fast is restarted on a compute node. """ - _name_ = 'node_update_started' + _name_ = 'node_system_restarted' - -class NodeUpdateFinished(BaseUpdateEvent): - """ - Called from the NodeUpdateView. - """ - _name_ = 'node_update_finished' + def __init__(self, node, **kwargs): + # Create such a task_id that info is send to SuperAdmins and node owner + task_id = task_id_from_string(node.owner.id, dummy=True, dc_id=DEFAULT_DC, tt=TT_DUMMY, tg=TG_DC_UNBOUND) + kwargs['node_hostname'] = node.hostname + super(NodeSystemRestarted, self).__init__(task_id, **kwargs) diff --git a/api/system/node/serializers.py b/api/system/node/serializers.py index cf3afa42..ed470e09 100644 --- a/api/system/node/serializers.py +++ b/api/system/node/serializers.py @@ -4,3 +4,4 @@ class NodeVersionSerializer(s.Serializer): hostname = s.Field() version = s.Field(source='system_version') + platform_version = s.Field(source='platform_version') diff --git a/api/system/node/views.py b/api/system/node/views.py index 32ee7e83..011f9b91 100644 --- a/api/system/node/views.py +++ b/api/system/node/views.py @@ -48,6 +48,8 @@ def system_node_version(request, hostname, data=None): * |async-no| :arg hostname: **required** - Node hostname :type hostname: string + :arg.data.fresh: Refresh cached node version information (default: false) + :type data.fresh: boolean :status 200: SUCCESS :status 403: Forbidden :status 404: Node not found @@ -111,11 +113,11 @@ def system_node_service_status(request, hostname, name, data=None): @request_data_defaultdc(permissions=(IsSuperAdmin,)) def system_node_update(request, hostname, data=None): """ - Install (:http:put:`PUT `) Danube Cloud update on a compute node. + Update (:http:put:`PUT `) Danube Cloud on a compute node. .. http:put:: /system/node/(hostname)/update - .. note:: The compute node software will be updated to the \ + .. note:: The compute node software should be updated to the \ same :http:get:`version ` as installed on the main Danube Cloud management VM. \ Use this API call after successful :http:put:`system update `. @@ -124,11 +126,13 @@ def system_node_update(request, hostname, data=None): :Permissions: * |SuperAdmin| :Asynchronous?: - * |async-no| + * |async-yes| :arg hostname: **required** - Node hostname :type hostname: string :arg data.version: **required** - git tag (e.g. ``v2.6.5``) or git commit to which the system should be updated :type data.version: string + :arg data.force: Whether to perform the update operation even though the software is already at selected version + :type data.force: boolean :arg data.key: X509 private key file used for authentication against EE git server. \ Please note that file MUST contain standard x509 file BEGIN/END header/footer. \ If not present, cached key file "update.key" will be used. @@ -138,14 +142,12 @@ def system_node_update(request, hostname, data=None): If not present, cached cert file "update.crt" will be used. :type data.cert: string :status 200: SUCCESS + :status 201: PENDING :status 400: FAILURE :status 403: Forbidden :status 404: Node not found - :status 417: Node update file is not available - :status 423: Node is not in maintenance state / Node version information could not be retrieved / \ -Task is already running + :status 423: Node is not in maintenance state / Node version information could not be retrieved :status 428: Node is already up-to-date - :status 504: Node worker is not responding """ return NodeUpdateView(request, hostname, data).put() diff --git a/api/system/service/control.py b/api/system/service/control.py index d404f2af..700b3ae0 100644 --- a/api/system/service/control.py +++ b/api/system/service/control.py @@ -181,7 +181,7 @@ class SystemReloadThread(Thread): """ Reload all app services in background. It is important to restart/reload the calling service as last one. - Used by system update and eslic. + Used by eslic. """ daemon = True diff --git a/api/system/update/api_views.py b/api/system/update/api_views.py index fd8406ee..8649b9ec 100644 --- a/api/system/update/api_views.py +++ b/api/system/update/api_views.py @@ -1,93 +1,63 @@ -import os from logging import getLogger -from django.conf import settings from api.api_views import APIView -from api.exceptions import TaskIsAlreadyRunning, PreconditionRequired +from api.exceptions import PreconditionRequired from api.system.messages import LOG_SYSTEM_UPDATE from api.system.update.serializers import UpdateSerializer -from api.system.update.events import SystemUpdateStarted, SystemUpdateFinished -from api.system.update.utils import process_update_reply -from api.system.service.control import SystemReloadThread -from api.task.response import SuccessTaskResponse, FailureTaskResponse -from que import TG_DC_UNBOUND, TT_DUMMY -from que.handlers import update_command -from que.utils import task_id_from_request -from que.lock import TaskLock -from vms.models import DefaultDc +from api.system.update.tasks import system_update +from api.task.response import FailureTaskResponse, mgmt_task_response +from que import TG_DC_UNBOUND logger = getLogger(__name__) -class UpdateResult(dict): - def __init__(self, update, success=False, rc=None, msg=None, **kwargs): - update = os.path.basename(update) - super(UpdateResult, self).__init__(update=update, success=success, rc=rc, msg=msg, **kwargs) - - @property - def log_detail(self): - return SuccessTaskResponse.dict_to_detail(self) - - class UpdateView(APIView): """ Update Danube Cloud application. """ + LOCK = 'system_update' dc_bound = False - _updates = () - _installed = 0 - _lock_key = 'system_update' def __init__(self, request, data): - super(UpdateView, self).__init__(request, force_default_dc=True) + super(UpdateView, self).__init__(request) self.data = data - self.user = request.user - self.task_id = task_id_from_request(self.request, dummy=True, tt=TT_DUMMY, tg=TG_DC_UNBOUND) - - def _update(self, version, key=None, cert=None): - logger.debug('Running system update to version: "%s"', version) - reply = update_command(version, key=key, cert=cert, sudo=not settings.DEBUG) - response_class, result = process_update_reply(reply, 'system', version) - response = response_class(self.request, result, task_id=self.task_id, msg=LOG_SYSTEM_UPDATE, - detail_dict=result, dc_bound=False) - - if response.status_code == 200: - # Restart all gunicorns and erigonesd! - SystemReloadThread(task_id=self.task_id, request=self.request, reason='system_update').start() - - return response @classmethod - def get_task_lock(cls): - # Also used in socket.io namespace - return TaskLock(cls._lock_key, desc='System task') + def is_task_running(cls): + return system_update.get_lock(cls.LOCK).exists() def put(self): - assert self.request.dc.id == DefaultDc().id + assert self.request.dc.is_default() ser = UpdateSerializer(self.request, data=self.data) if not ser.is_valid(): - return FailureTaskResponse(self.request, ser.errors, task_id=self.task_id, dc_bound=False) + return FailureTaskResponse(self.request, ser.errors, dc_bound=False) - version = ser.object['version'] + version = ser.data['version'] from core.version import __version__ as mgmt_version - # noinspection PyUnboundLocalVariable - if version == ('v' + mgmt_version): + if version == ('v' + mgmt_version) and not ser.data.get('force'): raise PreconditionRequired('System is already up-to-date') - lock = self.get_task_lock() - - if not lock.acquire(self.task_id, timeout=7200, save_reverse=False): - raise TaskIsAlreadyRunning - - try: - # Emit event into socket.io - SystemUpdateStarted(self.task_id, request=self.request).send() - - return self._update(version, key=ser.object.get('key'), cert=ser.object.get('cert')) - finally: - lock.delete(fail_silently=True, delete_reverse=False) - # Emit event into socket.io - SystemUpdateFinished(self.task_id, request=self.request).send() + obj = self.request.dc + msg = LOG_SYSTEM_UPDATE + _apiview_ = { + 'view': 'system_update', + 'method': self.request.method, + 'version': version, + } + meta = { + 'apiview': _apiview_, + 'msg': LOG_SYSTEM_UPDATE, + } + task_kwargs = ser.data.copy() + task_kwargs['dc_id'] = obj.id + + tid, err, res = system_update.call(self.request, None, (), kwargs=task_kwargs, meta=meta, + tg=TG_DC_UNBOUND, tidlock=self.LOCK) + if err: + msg = obj = None # Do not log an error here + + return mgmt_task_response(self.request, tid, err, res, msg=msg, obj=obj, api_view=_apiview_, dc_bound=False, + data=self.data, detail_dict=ser.detail_dict(force_full=True)) diff --git a/api/system/update/events.py b/api/system/update/events.py index 672eb76f..9a462d9c 100644 --- a/api/system/update/events.py +++ b/api/system/update/events.py @@ -1,26 +1,15 @@ -from api.event import Event +from api.event import BroadcastEvent -class BaseUpdateEvent(Event): +class SystemUpdateStarted(BroadcastEvent): """ - Base class for events below. - """ - def __init__(self, task_id, request=None, **kwargs): - if request: - kwargs['siosid'] = getattr(request, 'siosid', None) - - super(BaseUpdateEvent, self).__init__(task_id, **kwargs) - - -class SystemUpdateStarted(BaseUpdateEvent): - """ - Called from the UpdateView. + Called from the system_update task. Emitted to all socket.io sessions. """ _name_ = 'system_update_started' -class SystemUpdateFinished(BaseUpdateEvent): +class SystemUpdateFinished(BroadcastEvent): """ - Called from the UpdateView. + Called from the UpdateView. Emitted to all socket.io sessions. """ _name_ = 'system_update_finished' diff --git a/api/system/update/serializers.py b/api/system/update/serializers.py index 25bf0ab6..11fd83ed 100644 --- a/api/system/update/serializers.py +++ b/api/system/update/serializers.py @@ -6,9 +6,10 @@ class UpdateSerializer(s.Serializer): """ Validate update urls and login credentials. """ - version = s.CharField(required=True, max_length=1024, min_length=2) - key = s.CharField(required=False, max_length=1048576, validators=(validate_pem_key,)) - cert = s.CharField(required=False, max_length=1048576, validators=(validate_pem_cert,)) + version = s.SafeCharField(required=True, max_length=1024, min_length=2) + force = s.BooleanField(default=False) + key = s.CharField(required=False, max_length=102400, validators=(validate_pem_key,)) + cert = s.CharField(required=False, max_length=102400, validators=(validate_pem_cert,)) def __init__(self, request, *args, **kwargs): self.request = request diff --git a/api/system/update/tasks.py b/api/system/update/tasks.py new file mode 100644 index 00000000..23086028 --- /dev/null +++ b/api/system/update/tasks.py @@ -0,0 +1,45 @@ +from celery.utils.log import get_task_logger +from django.conf import settings + +from api.task.utils import mgmt_task, task_log_success +from api.system.update.utils import process_update_reply +from api.system.update.events import SystemUpdateStarted, SystemUpdateFinished +from que.erigonesd import cq +from que.exceptions import MgmtTaskException +from que.mgmt import MgmtTask +from que.handlers import update_command +from vms.models import Dc + +__all__ = ('system_update',) + +logger = get_task_logger(__name__) + + +# noinspection PyUnusedLocal +@cq.task(name='api.system.update.tasks.system_update', base=MgmtTask) +@mgmt_task(log_exception=True) +def system_update(task_id, dc_id=None, version=None, key=None, cert=None, force=None, **kwargs): + """ + Updated system on mgmt by running esdc-git-update. + """ + assert dc_id + assert version + + SystemUpdateStarted(task_id).send() # Send info to all active socket.io users + error = None + + try: + dc = Dc.objects.get_by_id(dc_id) + assert dc.is_default() + + reply = update_command(version, key=key, cert=cert, force=force, sudo=not settings.DEBUG, run=True) + result, error = process_update_reply(reply, 'system', version, logger=logger) + + if error: + raise MgmtTaskException(result['message']) + else: + task_log_success(task_id, kwargs['meta'].get('msg'), obj=dc, task_result=result) + + return result + finally: + SystemUpdateFinished(task_id, error=error).send() # Send info to all active socket.io users diff --git a/api/system/update/utils.py b/api/system/update/utils.py index c6927282..88dc545c 100644 --- a/api/system/update/utils.py +++ b/api/system/update/utils.py @@ -1,11 +1,9 @@ from logging import getLogger -from api.task.response import SuccessTaskResponse, FailureTaskResponse +app_logger = getLogger(__name__) -logger = getLogger(__name__) - -def process_update_reply(reply, target, version): +def process_update_reply(reply, target, version, logger=app_logger): """Handles reply which is output of the :func:`que.handlers.update_command` :return: Returns tuple of SuccessTaskResponse or FailureTaskResponse class object and result dictionary @@ -14,18 +12,18 @@ def process_update_reply(reply, target, version): rc = reply['returncode'] except (KeyError, TypeError): logger.error('Unexpected output from "%s" update: "%s"', target, reply) - response_class = FailureTaskResponse + error = True result = {'message': str(reply)} else: if rc == 0: msg = reply['stdout'] logger.info('Updating "%s" to version %s was successful: "%s"', target, version, msg) - response_class = SuccessTaskResponse + error = False else: msg = reply['stderr'] or reply['stdout'] - logger.error('Updating %s to version %s failed: "%s"', target, version, msg) - response_class = FailureTaskResponse + logger.error('Updating %s to version %s failed with rc=%s: "%s"', target, version, rc, msg) + error = True result = {'message': msg, 'returncode': rc} - return response_class, result + return result, error diff --git a/api/system/update/views.py b/api/system/update/views.py index b575cd5c..41edb130 100644 --- a/api/system/update/views.py +++ b/api/system/update/views.py @@ -9,7 +9,7 @@ @request_data_defaultdc(permissions=(IsSuperAdmin,)) def system_update(request, data=None): """ - Install (:http:put:`PUT `) Danube Cloud updates. + Update (:http:put:`PUT `) Danube Cloud to a selected version. .. http:put:: /system/update @@ -24,9 +24,11 @@ def system_update(request, data=None): :Permissions: * |SuperAdmin| :Asynchronous?: - * |async-no| + * |async-yes| :arg data.version: **required** - git tag (e.g. ``v2.6.5``) or git commit to which the system should be updated :type data.version: string + :arg data.force: Whether to perform the update operation even though the software is already at selected version + :type data.force: boolean :arg data.key: X509 private key file used for authentication against EE git server. \ Please note that file MUST contain standard x509 file BEGIN/END header/footer. \ If not present, cached key file "update.key" will be used @@ -36,9 +38,9 @@ def system_update(request, data=None): If not present, cached cert file "update.crt" will be used. :type data.cert: string :status 200: SUCCESS + :status 201: PENDING :status 400: FAILURE :status 403: Forbidden - :status 423: Task is already running :status 428: System is already up-to-date """ return UpdateView(request, data).response() diff --git a/api/tasks.py b/api/tasks.py index c069eba6..030b35a5 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -5,6 +5,8 @@ # noinspection PyUnresolvedReferences from api.system.tasks import * # noqa: F401,F403 # noinspection PyUnresolvedReferences +from api.system.update.tasks import * # noqa: F401,F403 +# noinspection PyUnresolvedReferences from api.task.tasks import * # noqa: F401,F403 # noinspection PyUnresolvedReferences from api.vm.tasks import * # noqa: F401,F403 diff --git a/api/utils/encoders.py b/api/utils/encoders.py index 951f1cc2..c238e9fc 100644 --- a/api/utils/encoders.py +++ b/api/utils/encoders.py @@ -89,4 +89,7 @@ def default(self, obj): pass elif hasattr(obj, '__iter__'): return tuple(item for item in obj) + # Bug in celery exception pickling + if isinstance(obj, Exception): + return '%s: %s' % (obj.__class__.__name__, six.text_type(obj)) return super(JSONEncoder, self).default(obj) diff --git a/bin/esdc-git-update b/bin/esdc-git-update index fccb7033..e4b19aac 100755 --- a/bin/esdc-git-update +++ b/bin/esdc-git-update @@ -1,45 +1,82 @@ #!/bin/bash # -# description: Script performs update of Danube Cloud to version specified as input -# parameter. +# description: Script performs update of Danube Cloud to version specified as +# input parameter. +# # input params: $1 - version tag or git commit hash that will be checked out -# $2 - full path to SSL private key -# $3 - full path to SSL public certificate +# $2 - full path to SSL private key (optional) +# $3 - full path to SSL public certificate (optional) +# --verbose - enable verbose output +# --force - continue update even if the HEAD points to the +# requested version (without git checkout) +# --esdc-service-restart - restart Danube Cloud services after +# successful update # -# author: Miroslav Bagljas -# email: miroslav.bagljas@erigones.com -# copyright: Copyright 2016-2017, Erigones, s. r. o. set -o pipefail # ############# # Return codes # ############# -GIT_NOT_FOUND=1 -PARAM_MISSING=2 -NO_MATCH=3 -UP_TO_DATE=4 -FAILED_FETCH=5 -FAILED_CHECKOUT=5 -POST_DEPLOY_FAILED=6 -UPGRADE_FAILED=6 - -# ##################### -# Positional arguments -# ##################### -VERSION=${1:-} -KEY_PATH=${2:-} -CERT_PATH=${3:-} +ERR_GIT_NOT_FOUND=1 +ERR_PARAM_MISSING=2 +ERR_NO_MATCH=3 +ERR_UP_TO_DATE=4 +ERR_FAILED_FETCH=5 +ERR_FAILED_CHECKOUT=5 +ERR_POST_DEPLOY_FAILED=6 +ERR_UPGRADE_FAILED=6 +ERR_LOCK_EXISTS=7 +ERR_LOCK_FAILED=8 +ERR_GIT_CMD=11 # ################# # Global variables # ################# MAINDIR="$(cd "$(dirname "$0")/.." || exit 64; pwd -P)" ERIGONES_HOME=${ERIGONES_HOME:-"${MAINDIR}"} +LOG_DIR="${ERIGONES_HOME}/var/log" +LOCK="${ERIGONES_HOME}/var/run/update.lock" CTLSH="${ERIGONES_HOME}/bin/ctl.sh" DEPLOY_CMD="${CTLSH} deploy --update" +SELF_CMD="${ERIGONES_HOME}/bin/$(basename "${0}")" APPLIANCE_UPDATE_CMD="${ERIGONES_HOME}/bin/esdc-appliance-update" NODE_UPDATE_CMD="${ERIGONES_HOME}/bin/esdc-node-update" +SERVICE_RESTART_CMD="${ERIGONES_HOME}/bin/esdc-service-control restart" +VERBOSE="" +RESTART="" +FORCE="" +VERSION="" +KEY_PATH="" +CERT_PATH="" + +# ##################### +# Positional arguments +# ##################### +for arg in "${@}"; do + case "${arg}" in + "--verbose"|"-v") + VERBOSE="true" + ;; + "--esdc-service-restart") + RESTART="true" + ;; + "--force") + FORCE="true" + ;; + *) + if [[ -z "${VERSION}" ]]; then + VERSION="${arg}" + elif [[ -z "${KEY_PATH}" ]]; then + KEY_PATH="${arg}" + elif [[ -z "${CERT_PATH}" ]]; then + CERT_PATH="${arg}" + else + break + fi + ;; + esac +done # ########## # Functions @@ -55,133 +92,244 @@ function die() { } function usage() { - echo "USAGE: $0 [private SSL key file] [X509 certificate file]" + echo "Usage: $0 [private SSL key file] [X509 certificate file]" echo "" echo "Example 1: $0 v2.4.0" echo "Example 2: $0 v3.0.0 /path/to/YourDCHost.key /path/to/YourDCHost.crt" } +function rollback() { + echo "#######################################" + echo "Running emergency rollback" + + if [[ -n "${DEPLOY_RUNNING_FILE}" && -f "${DEPLOY_RUNNING_FILE}" ]]; then + # Deploy post command failed -> we cannot rollback + we cannot revert the repository + rm -f "${DEPLOY_RUNNING_FILE}" + # TODO: cleanup not implemented + echo "WARNING: The post deploy command has failed and may have left the repository in an unpleasant state" + echo "WARNING: Manual cleanup is required" + else + # Deploy post command did not run yet + if [[ -n "${CURRENT_VERSION_REF}" && "${CURRENT_VERSION_REF}" != "${VERSION_REF}" ]]; then + echo "Running git checkout to commit before upgrade: ${CURRENT_VERSION_REF}" + git checkout "${CURRENT_VERSION_REF}" + fi + fi +} + # Change working directory to ERIGONES_HOME cd "${ERIGONES_HOME}" || exit 64 +function acquire_lock() { + if [[ -f "${LOCK}" ]]; then + die ${ERR_LOCK_EXISTS} "Locked! Maybe another upgrade is still running." + fi + + trap 'rm -f "${LOCK}"' EXIT + + if ! echo "$$" > "${LOCK}"; then + die ${ERR_LOCK_FAILED} "Could not acquire lock" + fi +} + if [[ -z "${ESDC_GIT_UPDATE_2ND_RUN}" ]]; then # Retrieve current Git settings if ! git --version >/dev/null; then - die $GIT_NOT_FOUND "Git was not found on system!" + die ${ERR_GIT_NOT_FOUND} "Git was not found on system!" fi - if [[ $# -ne 1 ]] && [[ $# -ne 3 ]]; then - usage - die $PARAM_MISSING + if [[ -z "${VERSION}" ]]; then + usage 1>&2 + die ${ERR_PARAM_MISSING} fi # Git environmental variables for cloning over HTTPS if [[ ! -z "${KEY_PATH}" ]]; then if [[ ! -f "${KEY_PATH}" ]]; then - die $PARAM_MISSING "File does not exist: ${KEY_PATH}" + die ${ERR_PARAM_MISSING} "File does not exist: ${KEY_PATH}" fi export GIT_SSL_KEY=${KEY_PATH} fi if [[ ! -z "${CERT_PATH}" ]]; then if [[ ! -f "${CERT_PATH}" ]]; then - die $PARAM_MISSING "File does not exist: ${CERT_PATH}" + die ${ERR_PARAM_MISSING} "File does not exist: ${CERT_PATH}" fi export GIT_SSL_CERT=${CERT_PATH} fi export GIT_SSL_NO_VERIFY="false" - # Gather information about current status of esdc git repository - CURRENT_VERSION=$(git rev-parse HEAD) - UPDATE_URL=$(git config --list | grep "remote.origin.url" | awk -F '=' '{print $2}') - # get most recent tag reachable from current commit - GIT_DESCRIBE=$(git describe) - MOST_RECENT_TAG=$(echo "${GIT_DESCRIBE}" | awk -F '-' '{print $1}') - COMMITS_AHEAD_TAG=$(echo "${GIT_DESCRIBE}" | awk -F '-' '{print $2}') + # Create lock file + acquire_lock - echo "#######################################" - echo "Remote git repository URL: ${UPDATE_URL}" - echo "Current HEAD points to commit: ${CURRENT_VERSION}" - echo "Most recent tag reachable: ${MOST_RECENT_TAG}" - echo "Number of commits ahead of recent tag: ${COMMITS_AHEAD_TAG}" - echo "SSL key file: ${KEY_PATH}" - echo "SSL certificate file: ${CERT_PATH}" - echo "#######################################" + CURRENT_VERSION_REF=$(git rev-parse HEAD || die ${ERR_GIT_CMD}) - # fetch most recent changes first - # MERGE will be done only if requested VERSION is among fetched objects - git fetch origin || die $FAILED_FETCH "Unable to fetch changes from git remote" + if [[ -n "${VERBOSE}" ]]; then + # Gather information about current status of esdc git repository + UPDATE_URL=$(git config --list | grep "remote.origin.url" | awk -F '=' '{print $2}') + # get most recent tag reachable from current commit + GIT_DESCRIBE=$(git describe) + MOST_RECENT_TAG=$(echo "${GIT_DESCRIBE}" | awk -F '-' '{print $1}') + COMMITS_AHEAD_TAG=$(echo "${GIT_DESCRIBE}" | awk -F '-' '{print $2}') - AVAILABLE_TAGS=$(git show-ref --tags) - # When VERSION matches one of the tags name in the list of reference-tags - # assign the commit reference to VERSION_REF variable - # else assume that VERSION holds value of the commit reference - if echo "${AVAILABLE_TAGS}" | awk '{print $NF}' | grep "refs/tags/${VERSION}$" >/dev/null; then - VERSION_REF=$(git rev-parse "${VERSION}") + echo "#######################################" + echo "Remote git repository URL: ${UPDATE_URL}" + echo "Current HEAD points to commit: ${CURRENT_VERSION_REF}" + echo "Most recent tag reachable: ${MOST_RECENT_TAG}" + echo "Number of commits ahead of recent tag: ${COMMITS_AHEAD_TAG}" + echo "SSL key file: ${KEY_PATH}" + echo "SSL certificate file: ${CERT_PATH}" + echo "Target version: ${VERSION}" + echo "#######################################" + fi + + if [[ "${VERSION}" == "_skip_checkout" ]]; then + echo "WARNING: Skipping git fetch and git checkout" + VERSION_REF="$(git rev-parse HEAD || die ${ERR_GIT_CMD})" else - if git rev-list --all | grep "^${VERSION}$" >/dev/null; then - VERSION_REF="${VERSION}" + echo "Running git fetch origin" + # fetch most recent changes first + # MERGE will be done only if requested VERSION is among fetched objects + git fetch origin || die ${ERR_FAILED_FETCH} "Unable to fetch changes from git remote" + + AVAILABLE_TAGS=$(git show-ref --tags) + # When VERSION matches one of the tags name in the list of reference-tags + # assign the commit reference to VERSION_REF variable + # else assume that VERSION holds value of the commit reference + if echo "${AVAILABLE_TAGS}" | awk '{print $NF}' | grep "refs/tags/${VERSION}$" >/dev/null; then + VERSION_REF=$(git rev-parse "${VERSION}") else - ERR_MSG="Could not match '${VERSION}' you requested with any of the commits. Aborting upgrade." - die $NO_MATCH "${ERR_MSG}" + if git rev-list --all | grep "^${VERSION}$" >/dev/null; then + VERSION_REF="${VERSION}" + else + ERR_MSG="Could not match '${VERSION}' you requested with any of the commits. Aborting upgrade." + die ${ERR_NO_MATCH} "${ERR_MSG}" + fi fi - fi - # if we are already at the requested revision exit - if [[ "${VERSION_REF}" == "$(git rev-parse HEAD)" ]]; then - ERR_MSG="Already at requested revision: ${VERSION} with hash: ${VERSION_REF}" - die $UP_TO_DATE "${ERR_MSG}" + # if we are already at the requested revision exit + if [[ "${VERSION_REF}" == "$(git rev-parse HEAD)" ]]; then + ERR_MSG="Already at requested revision: ${VERSION} with hash: ${VERSION_REF}" + + if [[ -n "${FORCE}" ]]; then + echo "${ERR_MSG}" + echo "WARNING: Skipping git checkout" + else + die ${ERR_UP_TO_DATE} "${ERR_MSG}" + fi + fi + + echo "Running git checkout to commit: ${VERSION_REF}" + ERR_MSG="Failed to checkout commit: ${VERSION_REF}" + git checkout "${VERSION_REF}" || die ${ERR_FAILED_CHECKOUT} "${ERR_MSG}" fi - ERR_MSG="Failed to checkout commit: ${VERSION_REF}" - git checkout "${VERSION_REF}" || die $FAILED_CHECKOUT "${ERR_MSG}" + # Set and truncate the log file + UPDATE_LOG="${LOG_DIR}/update.${VERSION_REF}.log" + echo "#######################################" | tee "${UPDATE_LOG}" + echo "Repository is on commit: ${VERSION_REF}" | tee -a "${UPDATE_LOG}" + echo "Starting 2nd run of ${SELF_CMD} ${*}" | tee -a "${UPDATE_LOG}" + echo "#######################################" | tee -a "${UPDATE_LOG}" + + export CURRENT_VERSION_REF + export VERSION_REF + + # Setup rollback + export DEPLOY_RUNNING_FILE="/tmp/.deploy.${VERSION_REF}.running" + rm -f "${DEPLOY_RUNNING_FILE}" 2> /dev/null + trap rollback ERR # Call our NEW self! - ESDC_GIT_UPDATE_2ND_RUN=true "${0}" "${@}" + ESDC_GIT_UPDATE_2ND_RUN="true" "${SELF_CMD}" "${@}" 2>&1 | tee -a "${UPDATE_LOG}" exit $? fi +# +# 2nd run starts +# + +# Double-check current commit +if [[ "${VERSION_REF}" != "$(git rev-parse HEAD || die ${ERR_GIT_CMD})" ]]; then + die ${ERR_GIT_CMD} "Git commit changed during update" +fi + +# In case we've upgraded from a version without locking +[[ ! "${LOCK}" ]] && acquire_lock + +# Deactivate our Git SSL configuration, because the upcoming commands may use git +unset GIT_SSL_KEY +unset GIT_SSL_CERT +unset GIT_SSL_NO_VERIFY + +# +# Appliance/node upgrade +# + if [[ "$(uname -s)" == "SunOS" ]] && [[ -f /usr/bin/zonename ]] && [[ "$(/usr/bin/zonename)" == "global" ]]; then DEPLOY_CMD="${DEPLOY_CMD} --node" # run node upgrade if [[ -x "${NODE_UPDATE_CMD}" ]]; then echo "Upgrading compute node software (please wait) ..." - node_update_output=$("${NODE_UPDATE_CMD}" 2>&1) + node_update_log="${LOG_DIR}/update_node.${VERSION_REF}.log" - # shellcheck disable=SC2181 - if [[ ${?} -eq 0 ]]; then + if "${NODE_UPDATE_CMD}" > "${node_update_log}" 2>&1; then echo "Compute node upgrade was successful" else - echo "${node_update_output}" >&2 - die $UPGRADE_FAILED "Compute node upgrade failed" + cat "${node_update_log}" >&2 + die ${ERR_UPGRADE_FAILED} "Compute node upgrade failed" fi fi else # run appliance upgrade if [[ -x "${APPLIANCE_UPDATE_CMD}" ]]; then echo "Upgrading appliances (please wait) ..." - appliance_update_output=$("${APPLIANCE_UPDATE_CMD}" 2>&1) + appliance_update_log="${LOG_DIR}/update_vms.${VERSION_REF}.log" - # shellcheck disable=SC2181 - if [[ ${?} -eq 0 ]]; then + if "${APPLIANCE_UPDATE_CMD}" > "${appliance_update_log}" 2>&1; then echo "Appliance upgrade was successful" else - echo "${appliance_update_output}" >&2 - die $UPGRADE_FAILED "Appliance upgrade failed" + cat "${appliance_update_log}" >&2 + die ${ERR_UPGRADE_FAILED} "Appliance upgrade failed" fi fi fi +# +# Post deploy +# + +echo "#######################################" +echo "Running post deploy command: ${DEPLOY_CMD}" + +# This will enable rollback +echo "$$" > "${DEPLOY_RUNNING_FILE}" + # Running ctl.sh deploy --update -deploy_cmd_res=$(${DEPLOY_CMD} 2>&1) +deploy_log="${LOG_DIR}/update_deploy.${VERSION_REF}.log" + +if ! ${DEPLOY_CMD} > "${deploy_log}" 2>&1; then + cat "${deploy_log}" >&2 + die ${ERR_POST_DEPLOY_FAILED} "Post deploy command failed" +fi + +# This will disable rollback +rm -f "${DEPLOY_RUNNING_FILE}" -# shellcheck disable=SC2181 -if [[ ${?} -eq 0 ]]; then - echo "Post deploy command was successful" - echo "${deploy_cmd_res}" | tail -n 7 +echo "Post deploy command was successful" +echo "#######################################" + +# +# Service restart +# + +if [[ -n "${RESTART}" ]]; then + echo "Going to restart all Danube Cloud system services in 10 seconds..." + nohup bash -c "sleep 10 && ${SERVICE_RESTART_CMD}" > /dev/null 2> /dev/null & else - echo "${deploy_cmd_res}" >&2 - die $POST_DEPLOY_FAILED "Post deploy command failed" + echo "You should now restart all Danube Cloud system services" + echo "(${SERVICE_RESTART_CMD})" fi + +exit 0 diff --git a/bin/esdc-service-control b/bin/esdc-service-control new file mode 100755 index 00000000..e0b1e6de --- /dev/null +++ b/bin/esdc-service-control @@ -0,0 +1,52 @@ +#!/bin/bash + +if [[ "$(id -u)" != "0" ]]; then + echo "This script must be run as root" >&2 + exit 6 +fi + +MAINDIR="$(cd "$(dirname "$0")/.." || exit 64 ; pwd -P)" +ERIGONES_HOME="${ERIGONES_HOME:-"${MAINDIR}"}" + +function node_status() { + svcs svc:/application/erigonesd:* +} + +function node_restart() { + svcadm restart svcs svc:/application/erigonesd:* +} + +function mgmt_status() { + systemctl status esdc@* erigonesd erigonesd-beat +} + +function mgmt_restart() { + systemctl restart esdc@* erigonesd erigonesd-beat +} + +function is_node() { + [[ "$(uname -s)" == "SunOS" ]] && \ + [[ -f /usr/bin/zonename ]] && \ + [[ "$(/usr/bin/zonename)" == "global" ]] +} + +case "$1" in + "status") + if is_node; then + node_status + else + mgmt_status + fi + ;; + "restart") + if is_node; then + node_restart + else + mgmt_restart + fi + ;; + *) + echo "Usage: $0 {status|restart}" >&2 + exit 1 + ;; +esac diff --git a/core/management/commands/_base.py b/core/management/commands/_base.py index 8ce02dc3..09bf54a6 100644 --- a/core/management/commands/_base.py +++ b/core/management/commands/_base.py @@ -52,6 +52,9 @@ class DanubeCloudCommand(BaseCommand): PROJECT_NAME = 'esdc-ce' CTLSH = os.path.join(PROJECT_DIR, 'bin', 'ctl.sh') + cmd_sha = 'git log --pretty=oneline -1 | cut -d " " -f 1' + cmd_tag = 'git symbolic-ref -q --short HEAD || git describe --tags --exact-match' + default_verbosity = 1 verbose = False strip_newline = False @@ -75,6 +78,13 @@ def get_version(self): from core.version import __version__ return 'Danube Cloud %s' % __version__ + def get_git_version(self): + with lcd(self.PROJECT_DIR): + _tag = self.local(self.cmd_tag, capture=True).strip().split('/')[-1] + _sha = self.local(self.cmd_sha, capture=True).strip() + + return _tag, _sha + def execute(self, *args, **options): """Set some default attributes before calling handle()""" self.verbose = int(options.get('verbosity', self.default_verbosity)) >= self.default_verbosity diff --git a/core/management/commands/deploy.py b/core/management/commands/deploy.py index 97fc61cf..454dc26d 100644 --- a/core/management/commands/deploy.py +++ b/core/management/commands/deploy.py @@ -15,23 +15,16 @@ def handle(self, que_only=False, update=False, **options): if que_only: if update: - self.display( - 'You can now restart the erigonesd service:\n' - '\tsvcadm restart svc:/application/erigonesd:*\n' - ) + self.display('You can now restart the erigonesd service:\n' + '\tesdc-service-control restart\n') else: self.display('You can now import the erigonesd SMF manifest (doc/init.d/erigonesd.xml)') else: if update: self.ctlsh('db_sync', '--force') self.ctlsh('post_update') - self.display( - 'You can now restart all Danube Cloud services:\n' - '\tsystemctl restart erigonesd.service\n' - '\tsystemctl restart esdc@gunicorn-api.service\n' - '\tsystemctl restart esdc@gunicorn-gui.service\n' - '\tsystemctl restart esdc@gunicorn-sio.service\n' - ) + self.display('You can now restart all Danube Cloud services:\n' + '\tesdc-service-control restart\n') else: self.ctlsh('secret_key') self.ctlsh('db_sync', '--init', '--force') diff --git a/core/management/commands/gendoc.py b/core/management/commands/gendoc.py index d5a4fa90..26d6c6f6 100644 --- a/core/management/commands/gendoc.py +++ b/core/management/commands/gendoc.py @@ -1,12 +1,13 @@ import os +import re -from ._base import DanubeCloudCommand, CommandOption, lcd +from ._base import DanubeCloudCommand, CommandOption, CommandError, lcd class Command(DanubeCloudCommand): help = 'Generate documentation files displayed in GUI.' DOC_REPO = 'https://github.com/erigones/esdc-docs.git' - DOC_TMP_DIR = '/tmp/esdc-docs' + DOC_TMP_DIR = '/var/tmp/esdc-docs' options = ( CommandOption('--api', '--api-only', action='store_true', dest='api_only', default=False, help='Generate only the API documentation.'), @@ -48,28 +49,43 @@ def gendoc_api(self): self.display('API documentation built successfully.', color='green') - def gendoc_user_guide(self): + def gendoc_user_guide(self, fallback_branch='master'): """Generate user guide""" doc_dst = self._path(self.PROJECT_DIR, 'gui', 'static', 'user-guide') with lcd(self.PROJECT_DIR): - branch = self.local('git rev-parse --abbrev-ref HEAD', capture=True).strip() - self.display('We are on branch "%s"' % branch) + try: + branch = self.get_git_version()[0] # Git tag or branch name + except CommandError: + self.display('Could not determine our branch or tag', color='yellow') + branch = fallback_branch + self.display('Falling back to "%s" branch' % branch, color='yellow') + else: + self.display('We are on branch "%s"' % branch) if self._path_exists(self.DOC_TMP_DIR): + existing_repo = True self.display('%s already exists in %s' % (self.DOC_REPO, self.DOC_TMP_DIR), color='yellow') with lcd(self.DOC_TMP_DIR): - self.local('git pull') + self.local('git fetch') self.display('%s has been successfully updated.' % self.DOC_REPO, color='green') else: + existing_repo = False self.local('git clone %s %s' % (self.DOC_REPO, self.DOC_TMP_DIR)) self.display('%s has been successfully cloned.' % self.DOC_TMP_DIR, color='green') with lcd(self.DOC_TMP_DIR): - if self.local('git checkout %s' % branch, raise_on_error=False) == 0: - self.display('Checked out esdc-docs branch "%s"' % branch, color='green') - else: - self.display('Could not checkout esdc-docs branch "%s"' % branch, color='red') + if self.local('git checkout %s' % branch, raise_on_error=False) != 0: + self.display('Could not checkout esdc-docs branch "%s"' % branch, color='yellow') + branch = fallback_branch + self.display('Falling back to "%s" branch' % branch, color='yellow') + self.local('git checkout %s' % branch) + + self.display('Checked out esdc-docs branch "%s"' % branch, color='green') + # If the branch is no a tag name, then we need to merge/pull + if existing_repo and not re.search('^v[0-9]', branch): + self.local('git merge --ff-only') + self.display('Merged esdc-docs branch "%s"' % branch, color='green') # Build sphinx docs with lcd(self._path(self.DOC_TMP_DIR, 'user-guide')): diff --git a/core/management/commands/git_version.py b/core/management/commands/git_version.py index 73f372d2..6d53689a 100644 --- a/core/management/commands/git_version.py +++ b/core/management/commands/git_version.py @@ -1,18 +1,10 @@ -from ._base import DanubeCloudCommand, lcd +from ._base import DanubeCloudCommand class Command(DanubeCloudCommand): help = 'Display last git commit hash.' - cmd_sha = 'git log --pretty=oneline -1 | cut -d " " -f 1' - cmd_tag = 'git symbolic-ref -q HEAD || git describe --tags --exact-match' default_verbosity = 2 - def _get_version(self, appdir): - with lcd(appdir): - _tag = self.local(self.cmd_tag, capture=True).strip().split('/')[-1] - _sha = self.local(self.cmd_sha, capture=True).strip() - - return '%s/%s' % (_tag, _sha) - def handle(self, *args, **options): - self.display('%s: %s' % (self.PROJECT_NAME, self._get_version(self.PROJECT_DIR))) + version = '%s/%s' % self.get_git_version() + self.display('%s: %s' % (self.PROJECT_NAME, version)) diff --git a/doc/api/es_bash_completion.sh b/doc/api/es_bash_completion.sh index 85515d7d..a955a5c2 100644 --- a/doc/api/es_bash_completion.sh +++ b/doc/api/es_bash_completion.sh @@ -178,7 +178,7 @@ _es() { ;; /system/node/*/update|/system/node/*/update/) - [[ "${action}" == "set" ]] && params="-version -key -cert" + [[ "${action}" == "set" ]] && params="-version -force -key -cert" ;; /system/node/*/*) @@ -208,7 +208,7 @@ _es() { ;; /system/update) - [[ "${action}" == "set" ]] && params="-version -key -cert" + [[ "${action}" == "set" ]] && params="-version -force -key -cert" ;; /system/settings/ssl-certificate) diff --git a/doc/changelog.rst b/doc/changelog.rst index 3e5aa078..8251413e 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -25,6 +25,7 @@ Features - Added experimental support for live migration - `#306 `__ - Added value size limit to metadata - `#321 `__ - Changed VM reboot action to perform a VM configuration update by default - `#328 `__ +- Changed system update API and added maintenance view with system update functionality into GUI - `#338 `__ - Added script for automating overlays creation - `#91 `__ Bugs diff --git a/gui/context_processors.py b/gui/context_processors.py index 06c02379..dff0692a 100644 --- a/gui/context_processors.py +++ b/gui/context_processors.py @@ -38,7 +38,9 @@ def common_stuff(request): """ Make common settings and variables available in templates. """ + from core.version import __version__, __edition__ from gui.node.utils import get_dc1_settings + from api.system.update.api_views import UpdateView return { 'settings': settings, @@ -49,4 +51,7 @@ def common_stuff(request): 'SOCKETIO_URL': settings.SOCKETIO_URL, 'THIRD_PARTY_JS': get_third_party_js(), 'THIRD_PARTY_CSS': get_third_party_css(), + 'SYSTEM_UPDATE_RUNNING': UpdateView.is_task_running(), + 'SYSTEM_VERSION': __version__, + 'SYSTEM_EDITION': __edition__ } diff --git a/gui/dc/urls.py b/gui/dc/urls.py index e82816fa..0effa616 100644 --- a/gui/dc/urls.py +++ b/gui/dc/urls.py @@ -34,4 +34,9 @@ # DC redirect to VM backups url(r'^switch/(?P[A-Za-z0-9\._-]+)/server/(?P[A-Za-z0-9\._-]+)/backup/$', 'dc_vm_backup', name='dc_vm_backup'), + # DC redirect do DC settings + url(r'^switch/(?P[A-Za-z0-9\._-]+)/settings/$', 'dc_dc_settings', + name='dc_dc_settings'), + url(r'^switch/dc-settings/$', 'dc_dc_settings', + name='dc_default_settings'), ) diff --git a/gui/dc/views.py b/gui/dc/views.py index 22e7c2c3..a91c0046 100644 --- a/gui/dc/views.py +++ b/gui/dc/views.py @@ -67,3 +67,14 @@ def dc_vm_backup(request, dc, hostname): dc_switch(request, dc) return redirect('vm_backup', hostname=hostname) + + +@login_required +@staff_required +def dc_dc_settings(request, dc=settings.VMS_DC_MAIN): + """ + Switch current datacenter and redirect to dc_settings page. + """ + dc_switch(request, dc) + + return redirect('dc_settings', query_string=request.GET) diff --git a/gui/forms.py b/gui/forms.py index 200c6005..9a3c8e86 100644 --- a/gui/forms.py +++ b/gui/forms.py @@ -3,7 +3,6 @@ from django import forms # noinspection PyProtectedMember from django.forms.forms import NON_FIELD_ERRORS -from django.utils.translation import ugettext from django.utils.six import iteritems, text_type from frozendict import frozendict @@ -253,7 +252,7 @@ def _set_api_errors(self, data): # Pair API errors to Django form errors for field in self.fields: if field in errors: - self._add_error(field, errors[field]) # should be lazy + self._add_error(field, errors.pop(field)) # should be lazy try: del self.cleaned_data[field] except KeyError: @@ -262,7 +261,9 @@ def _set_api_errors(self, data): if 'non_field_errors' in errors: self._add_error(NON_FIELD_ERRORS, errors['non_field_errors']) # should be lazy elif 'detail' in errors: - self._add_error(NON_FIELD_ERRORS, ugettext(errors['detail'])) # should be noop + self._add_error(NON_FIELD_ERRORS, errors['detail']) # should be noop + elif 'message' in errors: + self._add_error(NON_FIELD_ERRORS, errors['message']) # should be noop else: # More serious api error if isinstance(errors, list): # Maybe we have errors from multiple serializers diff --git a/gui/navigation.py b/gui/navigation.py index 17f5dd60..94dfd35f 100644 --- a/gui/navigation.py +++ b/gui/navigation.py @@ -4,11 +4,28 @@ STYLE_KEY = 'li_class' STYLE_KEY_DEFAULT = 'li_class_default' -DASHBOARD = { - 'title': _('Dashboard'), +SYSTEM = { + 'title': _('System'), 'icon': 'dashboard', - 'url': 'dashboard', - 'active_views': {'dashboard'}, + 'url': 'system_overview', + 'active_views': {'system_overview', 'system_settings', 'system_maintenance'}, + 'children': [ + { + 'title': _('Overview'), + 'icon': 'dashboard', + 'url': 'system_overview' + }, + { + 'title': _('Configuration'), + 'icon': 'cogs', + 'url': 'system_settings' + }, + { + 'title': _('Maintenance'), + 'icon': 'wrench', + 'url': 'system_maintenance' + }, + ] } DATACENTER = { @@ -17,7 +34,7 @@ 'url': 'dc_node_list', 'active_views': {'dc_list', 'dc_node_list', 'dc_storage_list', 'dc_network_list', 'dc_image_list', 'dc_template_list', 'dc_iso_list', 'dc_domain_list', 'dc_settings', 'dc_user_list', - 'dc_group_list', 'dc_user_profile'} + 'dc_group_list', 'dc_user_profile'}, # 'children': [] # Built on runtime } @@ -231,7 +248,7 @@ def __init__(self, request, dc_dns_only=False): if is_staff: if not dc_dns_only: DATACENTER['children'] += [DATACENTER_SETTINGS] - nav = [DATACENTER, NODES, SERVERS] # SuperAdmin + nav = [SYSTEM, DATACENTER, NODES, SERVERS] # SuperAdmin else: nav = [DATACENTER, SERVERS] # DCAdmin diff --git a/gui/static/gui/css/graphs.css b/gui/static/gui/css/graphs.css index 30144bbb..230a9922 100644 --- a/gui/static/gui/css/graphs.css +++ b/gui/static/gui/css/graphs.css @@ -17,6 +17,14 @@ border-top: none; } /* line 22, ../scss/graphs.scss */ +.chartable .legend.rows2 table { + height: 54px; +} +/* line 26, ../scss/graphs.scss */ +.chartable .legend.rows3 table { + height: 81px; +} +/* line 30, ../scss/graphs.scss */ .chartable .graph_history_control { padding: 0; margin-top: 2px; @@ -24,7 +32,7 @@ margin-left: 10px; margin-right: 10px; } -/* line 29, ../scss/graphs.scss */ +/* line 37, ../scss/graphs.scss */ .chartable .graph_history_control a.active { background: #46464d; background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzY2NjY3MCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzQ2NDY0ZCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=="); @@ -42,15 +50,15 @@ opacity: 0.90; filter: alpha(opacity=90); } -/* line 48, ../scss/graphs.scss */ +/* line 56, ../scss/graphs.scss */ .chartable.disabled > tbody > tr > td { background: #C0C0C0; } -/* line 52, ../scss/graphs.scss */ +/* line 60, ../scss/graphs.scss */ .chartable.problem > tbody > tr > td { background: #FF6666; } -/* line 56, ../scss/graphs.scss */ +/* line 64, ../scss/graphs.scss */ .chartable .period-wrap { font-size: 10px; color: #545454; @@ -58,12 +66,36 @@ text-align: center; } -/* line 64, ../scss/graphs.scss */ +/* line 72, ../scss/graphs.scss */ +.graph-container { + position: relative; + height: 260px; +} + +/* line 77, ../scss/graphs.scss */ .graph { width: 100%; height: 260px; } -/* line 68, ../scss/graphs.scss */ +/* line 81, ../scss/graphs.scss */ .graph.tall { height: 500px; } + +/* line 86, ../scss/graphs.scss */ +.graph-center { + position: absolute; + top: 125px; + left: 0; + width: 100%; + font-size: 48px; +} + +/* line 94, ../scss/graphs.scss */ +.graph-item-label { + font-size: 14px; + line-height: 18px; + text-align: center; + padding: 2px; + color: #fff; +} diff --git a/gui/static/gui/css/tables.css b/gui/static/gui/css/tables.css index 4cae5524..2197062b 100644 --- a/gui/static/gui/css/tables.css +++ b/gui/static/gui/css/tables.css @@ -16,70 +16,74 @@ td .wrap, th .wrap { white-space: normal; } -/* line 27, ../scss/tables.scss */ +/* line 24, ../scss/tables.scss */ +td.center-text { + text-align: center; +} +/* line 30, ../scss/tables.scss */ td .button.mini { padding: 0px 8px; margin: 0; font-weight: normal; } -/* line 32, ../scss/tables.scss */ +/* line 35, ../scss/tables.scss */ td .button.mini i { padding-top: 0.5px; vertical-align: middle; } -/* line 37, ../scss/tables.scss */ +/* line 40, ../scss/tables.scss */ td .button.small { padding: 2px 10px; margin: -2px 0; } -/* line 41, ../scss/tables.scss */ +/* line 44, ../scss/tables.scss */ td .button.small i { padding-top: 0.5px; vertical-align: middle; } -/* line 48, ../scss/tables.scss */ +/* line 51, ../scss/tables.scss */ td.button-group .button.small { margin: 0; margin-top: 1px; } -/* line 53, ../scss/tables.scss */ +/* line 56, ../scss/tables.scss */ td .input-error { color: #c94a48; font-style: bold; } -/* line 59, ../scss/tables.scss */ +/* line 62, ../scss/tables.scss */ td .help-inline.error { padding-top: 0; margin-bottom: -8px; } -/* line 63, ../scss/tables.scss */ +/* line 66, ../scss/tables.scss */ td .help-inline.note { padding-top: 0; margin-bottom: -8px; line-height: 14px; color: #888888; } -/* line 71, ../scss/tables.scss */ +/* line 74, ../scss/tables.scss */ td .msg { padding-top: 10px; text-align: center; font-weight: bold; } -/* line 78, ../scss/tables.scss */ +/* line 81, ../scss/tables.scss */ .nowrap tr td { white-space: nowrap; } -/* line 80, ../scss/tables.scss */ +/* line 83, ../scss/tables.scss */ .nowrap tr td.wrap { white-space: normal; } -/* line 85, ../scss/tables.scss */ +/* line 88, ../scss/tables.scss */ table.box { background-color: #FAFAFA; } -/* line 89, ../scss/tables.scss */ +/* line 92, ../scss/tables.scss */ table.box thead, table.box tfoot { background: #e5e5e5; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ViZWJlYiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2U1ZTVlNSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); @@ -94,28 +98,28 @@ table.box thead, table.box tfoot { -webkit-border-radius: 3px 3px; border-radius: 3px 3px 0 0 / 3px 3px 0 0; } -/* line 96, ../scss/tables.scss */ +/* line 99, ../scss/tables.scss */ table.box.table-striped tbody tr:nth-child(odd) td, table.box.table-striped tbody tr:nth-child(odd) th { background: #FDFDFD; } -/* line 100, ../scss/tables.scss */ +/* line 103, ../scss/tables.scss */ table.box.break-words { word-wrap: break-word; word-break: break-all; } -/* line 106, ../scss/tables.scss */ +/* line 109, ../scss/tables.scss */ table.box.table-striped tbody tr.info td { background-color: #d9edf7; } -/* line 109, ../scss/tables.scss */ +/* line 112, ../scss/tables.scss */ table.box.table-striped tbody tr.error td { background-color: #f2dede; } -/* line 114, ../scss/tables.scss */ +/* line 117, ../scss/tables.scss */ table.box .button { line-height: normal; } -/* line 115, ../scss/tables.scss */ +/* line 118, ../scss/tables.scss */ table.box .button i { font-size: 14px; } @@ -123,32 +127,32 @@ table.box .button i { /* * Responsive tables */ -/* line 127, ../scss/tables.scss */ +/* line 130, ../scss/tables.scss */ table.table-responsive th, table.table-responsive td { word-wrap: break-word; max-width: 200px; } -/* line 132, ../scss/tables.scss */ +/* line 135, ../scss/tables.scss */ .tab-header { height: auto; word-wrap: break-word; overflow: hidden; } -/* line 138, ../scss/tables.scss */ +/* line 141, ../scss/tables.scss */ p.table_desc { margin: 0; color: #454545; } -/* line 143, ../scss/tables.scss */ +/* line 146, ../scss/tables.scss */ div.dataTables_paginate { float: none; margin-bottom: 20px; padding-right: 10px; } -/* line 149, ../scss/tables.scss */ +/* line 152, ../scss/tables.scss */ div.dataTables_paginate a { background: #ededed; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2VkZWRlZCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); @@ -202,31 +206,31 @@ div.dataTables_paginate a:active { background-image: -webkit-linear-gradient(top, #d6d6d6, #e8e8e8); background-image: linear-gradient(to bottom, #d6d6d6, #e8e8e8); } -/* line 157, ../scss/tables.scss */ +/* line 160, ../scss/tables.scss */ div.dataTables_paginate .first { -moz-border-radius: 4px 0 0 4px; -webkit-border-radius: 4px; border-radius: 4px 0 0 4px; } -/* line 161, ../scss/tables.scss */ +/* line 164, ../scss/tables.scss */ div.dataTables_paginate .last { -moz-border-radius: 0 4px 4px 0; -webkit-border-radius: 0; border-radius: 0 4px 4px 0; } -/* line 165, ../scss/tables.scss */ +/* line 168, ../scss/tables.scss */ div.dataTables_paginate .ui-state-disabled, div.dataTables_paginate .ui-state-disabled:hover { background: #ddd; cursor: default; text-shadow: none; } -/* line 173, ../scss/tables.scss */ +/* line 176, ../scss/tables.scss */ .inner-well .table th, .inner-well .table td { border: none; } -/* line 178, ../scss/tables.scss */ +/* line 181, ../scss/tables.scss */ #debug { position: fixed; bottom: 0; @@ -237,13 +241,13 @@ div.dataTables_paginate .ui-state-disabled, div.dataTables_paginate .ui-state-di padding: 0 5px; } -/* line 188, ../scss/tables.scss */ +/* line 191, ../scss/tables.scss */ table#debug-query-table tr td { padding-top: 0; padding-bottom: 0; } -/* line 193, ../scss/tables.scss */ +/* line 196, ../scss/tables.scss */ table#debug-query-table code { border: 0; margin: 0; diff --git a/gui/static/gui/js/gsio.js b/gui/static/gui/js/gsio.js index 44859a4a..cca61d06 100644 --- a/gui/static/gui/js/gsio.js +++ b/gui/static/gui/js/gsio.js @@ -577,6 +577,12 @@ function message_callback(code, res, view, method, args, kwargs, apiview, apidat // Update image list image_list_update(kwargs.name, false, 2, 'pending'); break; + + case 'system_node_update': // system_node_update started + if (SYSTEM_UPDATE && SYSTEM_UPDATE.is_displayed()) { + SYSTEM_UPDATE.started(hostname); + } + break; } // all FAILURE -> send notification @@ -763,6 +769,33 @@ function _task_event_callback(result) { alert2(result.message); break; + case 'node_system_restarted': + if (result.system_version) { + // If there is a node system version label, then we need to update it + node_system_version_update(result.hostname, result.system_version); + } + break; + + case 'system_update_started': // Broadcast event + if (SYSTEM_UPDATE && SYSTEM_UPDATE.is_displayed()) { + SYSTEM_UPDATE.started(); + system_update_started(); + } else { + system_update_started(); + return false; // do not update cached tasklog + } + break; + + case 'system_update_finished': // Broadcast event + if (SYSTEM_UPDATE && SYSTEM_UPDATE.is_displayed()) { + SYSTEM_UPDATE.finished(null, result.error); + system_update_finished(result.error); + } else { + system_update_finished(result.error); + return false; // do not update cached tasklog + } + break; + case 'user_current_dc_changed': alert2(gettext('Your current datacenter was changed. Please log out and log in again or navigate to the default page.')); break; @@ -1120,6 +1153,12 @@ function _task_status_callback(res, apiview) { } break; // always update cached tasklog + + case 'system_node_update': // system_node_update finished + if (SYSTEM_UPDATE && SYSTEM_UPDATE.is_displayed()) { + SYSTEM_UPDATE.finished(hostname, (res.status !== 'SUCCESS')); + } + break; // update cached tasklog } return true; // true => update cached_tasklog diff --git a/gui/static/gui/js/helpers.js b/gui/static/gui/js/helpers.js index f17fc047..4fd5c6e4 100644 --- a/gui/static/gui/js/helpers.js +++ b/gui/static/gui/js/helpers.js @@ -345,18 +345,29 @@ function _ajax_file_upload(file_input, datatype, method, url, success_handler, d } var backdrop = get_loading_screen(null, dim); + var form_data = new FormData(); + + if (typeof(data) !== 'undefined') { + for (var i=0; i < data.length; i++) { + form_data.append(data[i].name, data[i].value); + } + } + + file_input.slice(1).each(function(index, element) { + for (var i=0; i < element.files.length; i++) { + form_data.append($(this).attr('name'), element.files[i], element.files[i].name); + } + }); + var kwargs = { type: method, url: url, dataType: datatype, files: file_input[0].files, + formData: form_data, }; - if (typeof(data) !== 'undefined') { - kwargs.formData = data; - } - - file_input.one('fileuploadsend', function(e, data) { _ajax_before_send(btn, backdrop); }); + file_input.one('fileuploadsend', function() { _ajax_before_send(btn, backdrop); }); AJAX = file_input.fileupload('send', kwargs) .error(function(jqXHR, textStatus, errorThrown) { ajax_error_handler(jqXHR, textStatus, errorThrown); }) diff --git a/gui/static/gui/js/system/maintenance.js b/gui/static/gui/js/system/maintenance.js new file mode 100644 index 00000000..0a36c7b0 --- /dev/null +++ b/gui/static/gui/js/system/maintenance.js @@ -0,0 +1,166 @@ +var SYSTEM_VERSION_URL = 'https://danubecloud.org/api/releases'; +var SYSTEM_UPDATE = null; +var SYSTEM_UPDATE_LOADING = null; +var SYSTEM_UPDATE_RESTART_DELAY = 10; + +function get_available_system_versions(handler) { + ajax_json('GET', SYSTEM_VERSION_URL + '?system_version=' + SYSTEM_VERSION, ATIMEOUT, handler); +} + +function get_latest_system_version(handler) { + get_available_system_versions(function(data) { + handler(data[0]); + }); +} + +function hide_system_update_loading_screen() { + if (SYSTEM_UPDATE_LOADING) { + SYSTEM_UPDATE_LOADING.detach(); + SYSTEM_UPDATE_LOADING = null; + } +} + +function show_system_update_loading_screen() { + SYSTEM_UPDATE_LOADING = get_loading_screen('System update in progress (please wait)', true, true); + SYSTEM_UPDATE_LOADING.appendTo(document.body); +} + +function _system_update_started() { + // Hide previous update loading screen + hide_system_update_loading_screen(); + // Hide other loading screens + hide_loading_screen(); + // Show update loading screen + show_system_update_loading_screen(); +} + +function _system_update_finished() { + // Hide other loading screens + hide_loading_screen(); + // Hide update loading screen + hide_system_update_loading_screen(); +} + +// Run after we get a broadcast event +function system_update_started() { + SYSTEM_UPDATE_RUNNING = true; + _system_update_started(); +} + +// Run after we get a broadcast event +function system_update_finished(error) { + SYSTEM_UPDATE_RUNNING = false; + + if (error) { + _system_update_finished(); + } else { + // A complete system restart will follow in 10 seconds + setTimeout(function() { + _system_update_finished(); + alert2(gettext('The system was restarted. Please refresh your browser.')); + }, SYSTEM_UPDATE_RESTART_DELAY * 1000); + } +} + +// Always run at each page load +function system_update_check() { + if (SYSTEM_UPDATE_RUNNING) { + _system_update_started(); + } +} + +// Update node version span +function node_system_version_update(hostname, version) { + var version_span = $(jq('node_system_version_' + hostname)); // escaping dots in hostname + + if (!version_span.length) { + return; + } + + version_span.text(version); +} + +// Update system version span +function system_version_update(version) { + var version_span = $(jq('system_version')); + + if (!version_span.length) { + return; + } + + version_span.text(version); +} + +function SystemUpdate() { + var self = this; + var modal = null; + + this.is_displayed = function() { + return Boolean($('#system_update_modal').length); + }; + + this.started = function(hostname) { + // Hide modal if displayed + if (self.modal) { + self.modal.mod.modal('hide'); + } + }; + + this.finished = function(hostname, error) { + // Notify user + if (error) { + if (hostname) { + notify('error', interpolate(gettext('System update on node %s failed (see task log in the main DC)'), [hostname])); + } else { + notify('error', gettext('System update failed (see task log in the main DC)')); + } + } else { + if (hostname) { + notify('success', interpolate(gettext('System update on node %s successfully finished
(Please wait for the node software to be restarted)'), [hostname])); + node_system_version_update(hostname, '...'); + } else { + notify('success', gettext('System update successfully finished
(Please wait for the system software to be restarted)')); + system_version_update('...'); + } + } + }; + + function enable_latest_system_version_btn() { + $('a.latest_system_version').click(function() { + var btn = $(this); + + get_latest_system_version(function(data) { + if (data.name) { + btn.prev().val(data.name); + } + }); + + return false; + }); + } + + /******** START *********/ + NODE_LIST = new NodeList({}); + + $('#system_update').click(function() { + self.modal = new obj_form_modal($(this), '#system_update_modal', function(mod, start) { + enable_latest_system_version_btn(); + }); + + return false; + }); + + $('#node_system_update').click(function() { + self.modal = new obj_form_modal($(this), '#node_update_modal', function(mod, start) { + if (start) { + var hostnames = NODE_LIST.get_hostnames(); + $('#id_node-hostnames_text').html(hostnames.join(', ')); + $('#id_node-hostnames').val(hostnames); + } + enable_latest_system_version_btn(); + }); + + return false; + }); +} // SystemUpdate + diff --git a/gui/static/gui/scss/graphs.scss b/gui/static/gui/scss/graphs.scss index 1a332000..ebb6eea8 100644 --- a/gui/static/gui/scss/graphs.scss +++ b/gui/static/gui/scss/graphs.scss @@ -19,6 +19,14 @@ } } + .legend.rows2 table { + height: 54px; + } + + .legend.rows3 table { + height: 81px; + } + .graph_history_control { padding: 0; margin-top: 2px; @@ -61,6 +69,11 @@ } } +.graph-container { + position: relative; + height: 260px; +} + .graph { width: 100%; height: 260px; @@ -69,3 +82,19 @@ height: 500px; } } + +.graph-center { + position: absolute; + top: 125px; + left: 0; + width: 100%; + font-size: 48px; +} + +.graph-item-label { + font-size: 14px; + line-height: 18px; + text-align: center; + padding: 2px; + color: #fff; +} diff --git a/gui/static/gui/scss/tables.scss b/gui/static/gui/scss/tables.scss index 4877130a..dd3b0adb 100644 --- a/gui/static/gui/scss/tables.scss +++ b/gui/static/gui/scss/tables.scss @@ -21,6 +21,9 @@ td, th { } td { + &.center-text { + text-align: center; + } .button { diff --git a/gui/system/__init__.py b/gui/system/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gui/system/forms.py b/gui/system/forms.py new file mode 100644 index 00000000..e9e97b39 --- /dev/null +++ b/gui/system/forms.py @@ -0,0 +1,85 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from gui.forms import SerializerForm +from gui.fields import ArrayField +from api.system.update.views import system_update +from api.system.node.views import system_node_update + + +class UpdateForm(SerializerForm): + """ + Update the system on mgmt to a new version. + """ + _api_call = system_update + _update_version = None + _update_force = None + _update_cert = None + _update_key = None + + version = forms.CharField(label=_('Target version'), required=True, + widget=forms.TextInput(attrs={'class': 'input-transparent narrow'}), + help_text=_('Version tag or commit hash to which system should be updated. ' + 'NOTE: The version tag is usually prefixed with a "v" character.')) + force = forms.BooleanField(label=_('Force update?'), required=False, + widget=forms.CheckboxInput(attrs={'class': 'normal-check'}), + help_text=_('Force update even though the software is already at selected version.')) + cert = forms.FileField(label=_('Update certificate'), required=False, allow_empty_file=False, max_length=65536, + help_text=_('X509 private certificate file used for authentication against EE git server.')) + key = forms.FileField(label=_('Update key'), required=False, allow_empty_file=False, max_length=65536, + help_text=_('X509 private key file used for authentication against EE git server.')) + + def _final_data(self, data=None): + if self._update_version is None: + self._update_version = data['version'] + + if self._update_force is None: + self._update_force = data['force'] + + api_data = {'version': self._update_version, 'force': self._update_force} + + if self._update_cert is None: + if data.get('cert'): + self._update_cert = data['cert'].read() + else: + self._update_cert = '' + + if self._update_cert: + api_data['cert'] = self._update_cert + + if self._update_key is None: + if data.get('key'): + self._update_key = data['key'].read() + else: + self._update_key = '' + + if self._update_key: + api_data['key'] = self._update_key + + return api_data + + def call_system_update(self): + return self.save(action='update', args=()) + + +class NodeUpdateForm(UpdateForm): + """ + Update the system on compute nodes to a new version. + """ + _api_call = system_node_update + _node_hostname = None + + hostnames = ArrayField(required=True, widget=forms.HiddenInput(attrs={'class': 'hide'})) + + def _add_error(self, field_name, error): + if self._node_hostname: + if isinstance(error, (list, tuple)): + error = ['%s: %s' % (self._node_hostname, err) for err in error] + else: + error = '%s: %s' % (self._node_hostname, error) + return super(NodeUpdateForm, self)._add_error(field_name, error) + + def call_system_node_update(self, hostname): + # Save current node hostname for __add_error() + self._node_hostname = hostname + return self.save(action='update', args=(hostname,)) diff --git a/gui/system/urls.py b/gui/system/urls.py new file mode 100644 index 00000000..fd0a0b4c --- /dev/null +++ b/gui/system/urls.py @@ -0,0 +1,13 @@ +from django.conf.urls import patterns, url +from django.views.generic import RedirectView + +urlpatterns = patterns( + 'gui.system.views', + + url(r'^$', RedirectView.as_view(url='/system/overview/', permanent=False)), + url(r'^overview/$', 'overview', name='system_overview'), + url(r'^settings/$', 'settings', name='system_settings'), + url(r'^maintenance/$', 'maintenance', name='system_maintenance'), + url(r'^maintenance/update/form/$', 'system_update_form', name='system_update_form'), + url(r'^maintenance/node-update/form/$', 'system_node_update_form', name='system_node_update_form'), +) diff --git a/gui/system/utils.py b/gui/system/utils.py new file mode 100644 index 00000000..c43d73f7 --- /dev/null +++ b/gui/system/utils.py @@ -0,0 +1,12 @@ +from vms.utils import AttrDict + + +class GraphItem(AttrDict): + """ + Representation of one item in a pie chart. + """ + def __init__(self, label, data, **options): + dict.__init__(self) + self['label'] = label + self['data'] = data + self.update(options) diff --git a/gui/system/views.py b/gui/system/views.py new file mode 100644 index 00000000..463f46bc --- /dev/null +++ b/gui/system/views.py @@ -0,0 +1,108 @@ +from django.shortcuts import render, redirect +from django.views.decorators.http import require_POST +from django.contrib.auth.decorators import login_required + +from gui.decorators import staff_required, ajax_required +from gui.utils import collect_view_data +from gui.node.forms import NodeStatusForm +from gui.system.utils import GraphItem +from gui.system.forms import UpdateForm, NodeUpdateForm +from api.utils.views import call_api_view +from api.system.stats.api_views import SystemStatsView +from api.system.base.views import system_version +from vms.models import Node + + +NODE_STATS_COLORS = { + 'maintenance': '#684688', + 'online': '#468847', + 'unreachable': '#f89406', + 'unlicensed': '#999999', +} + +VM_STATS_COLORS = { + 'notcreated': '#999999', + 'stopped': '#b94a48', + 'running': '#468847', + 'frozen': '#ddffff', + 'unknown': '#333333', +} + + +@login_required +@staff_required +def overview(request): + """ + Overview/stats of the whole system. + """ + context = collect_view_data(request, 'system_overview') + context['stats'] = stats = SystemStatsView.get_stats() + stats['dcs'] = [GraphItem(label, data) for label, data in stats['dcs'].items()] + stats['nodes'] = [GraphItem(label, data, color=NODE_STATS_COLORS[label]) for label, data in stats['nodes'].items()] + stats['vms'] = [GraphItem(label, data, color=VM_STATS_COLORS[label]) for label, data in stats['vms'].items()] + + return render(request, 'gui/system/overview.html', context) + + +@login_required +@staff_required +def settings(request): + """ + System settings. + """ + context = collect_view_data(request, 'system_settings') + + return render(request, 'gui/system/settings.html', context) + + +@login_required +@staff_required +def maintenance(request): + """ + System maintenance. + """ + context = collect_view_data(request, 'system_maintenance') + context['system'] = call_api_view(request, 'GET', system_version).data.get('result', {}) + context['node_list'] = Node.all() + context['current_view'] = 'maintenance' + context['status_form'] = NodeStatusForm(request, None) + context['update_form'] = UpdateForm(request, None) + context['node_update_form'] = NodeUpdateForm(request, None, prefix='node') + + return render(request, 'gui/system/maintenance.html', context) + + +@login_required +@staff_required +@ajax_required +@require_POST +def system_update_form(request): + """ + Ajax page for running system update on mgmt VM. + """ + form = UpdateForm(request, None, request.POST, request.FILES) + + if form.is_valid(): + if form.call_system_update() == 201: + return redirect('system_maintenance') + + return render(request, 'gui/system/update_form.html', {'form': form}) + + +@login_required +@staff_required +@ajax_required +@require_POST +def system_node_update_form(request): + """ + Ajax page for running system update on selected compute nodes. + """ + form = NodeUpdateForm(request, None, request.POST, request.FILES, prefix='node') + + if form.is_valid(): + res = [form.call_system_node_update(hostname) == 201 for hostname in form.cleaned_data['hostnames']] + + if all(res): + return redirect('system_maintenance') + + return render(request, 'gui/system/node_update_form.html', {'form': form}) diff --git a/gui/templates/base.html b/gui/templates/base.html index b2f378ad..ca05e69c 100644 --- a/gui/templates/base.html +++ b/gui/templates/base.html @@ -74,6 +74,9 @@ var USER_VMS_TAGS = '{{ request.user.vms_tags|escapejs }}'; var CURRENT_DC = '{{ request.dc.name }}'; var CURRENT_TZ = '{{ TIMEZONE }}'; + var SYSTEM_VERSION = '{{ SYSTEM_VERSION }}'; + var SYSTEM_EDITION = '{{ SYSTEM_EDITION }}'; + var SYSTEM_UPDATE_RUNNING = {{ SYSTEM_UPDATE_RUNNING|yesno:"true,false" }}; {% compress js %} @@ -83,6 +86,7 @@ + @@ -132,6 +136,7 @@ + {% for jslib in THIRD_PARTY_JS %} diff --git a/gui/templates/gui/dashboard.html b/gui/templates/gui/dashboard.html deleted file mode 100644 index 9454e974..00000000 --- a/gui/templates/gui/dashboard.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends load_base %} - -{% block base_content %} -
- -
- - -
- -
-{% endblock %} diff --git a/gui/templates/gui/navigation.html b/gui/templates/gui/navigation.html index bab01c1a..93d300f1 100644 --- a/gui/templates/gui/navigation.html +++ b/gui/templates/gui/navigation.html @@ -8,6 +8,7 @@ update_tasks_link(); dc_switch_link(); dc_check('{{ request.dc.name }}'); + system_update_check(); activate_nav_collapse(); }); diff --git a/gui/templates/gui/system/maintenance.html b/gui/templates/gui/system/maintenance.html new file mode 100644 index 00000000..ff2b2ad4 --- /dev/null +++ b/gui/templates/gui/system/maintenance.html @@ -0,0 +1,138 @@ +{% extends load_base %} +{% load i18n %} + +{% block javascript %} + +{% endblock %} + +{% block base_content %} +{% include "gui/node/status_modal.html" %} +{% include "gui/system/update_modal.html" %} +{% include "gui/system/node_update_modal.html" %} + +
+ +
+
+
+
+ {% trans "System maintenance" %} +
+
+ + + + + + + + + + + + + + + + +
{% trans "Hostname" %}{{ system.hostname }}
{% trans "DC Version" %}{{ system.version }}
+ +
+
+
+
+
+ +
+
+
+
+ {% trans "Compute nodes maintenance" %} +
+ + + {% if node_list %} + + + + + + + + + + + + + + {% endif %} + + + {% for node in node_list %} + + + + + + + + + + {% endfor %} + + + + + + +
+ + + +
+
+ + +
+
{% trans "Hostname" %}{% trans "Vendor" %}{% trans "Model" %}{% trans "Status" %}{% trans "DC Version" %}{% trans "Platform Version" %}
+
+ + +
+
+ {{ node.hostname }} + + {{ node.vendor }} + + {{ node.model }} + + {% with status_display=node.get_status_display %} + {{ status_display }} + {% endwith %} + + {{ node.system_version }} + + {{ node.platform_version }} +
+ + {% blocktrans count node_len=node_list|length %}Selected 0 of {{ node_len }} node + {% plural %}Selected 0 of {{ node_len }} nodes{% endblocktrans %} + + +
+
+
+
+ +
+{% endblock %} diff --git a/gui/templates/gui/system/node_update_form.html b/gui/templates/gui/system/node_update_form.html new file mode 100644 index 00000000..70095696 --- /dev/null +++ b/gui/templates/gui/system/node_update_form.html @@ -0,0 +1,22 @@ +{% load i18n %} +{% csrf_token %} +{% for error in form.non_field_errors %} +
{{ error }}
+{% endfor %} + +
+
+ +
+ {{ form.hostnames }} +
{{ form.hostnames.value }}
+
+
+
+ +{% include "gui/form_field.html" with field=form.version addon='Get latest version' %} +{% include "gui/form_field_checkbox.html" with field=form.force %} +{% if SYSTEM_EDITION == "ee" %} +{% include "gui/form_field.html" with field=form.cert %} +{% include "gui/form_field.html" with field=form.key %} +{% endif %} diff --git a/gui/templates/gui/system/node_update_modal.html b/gui/templates/gui/system/node_update_modal.html new file mode 100644 index 00000000..78956523 --- /dev/null +++ b/gui/templates/gui/system/node_update_modal.html @@ -0,0 +1,21 @@ +{% load i18n %} + diff --git a/gui/templates/gui/system/overview.html b/gui/templates/gui/system/overview.html new file mode 100644 index 00000000..00124566 --- /dev/null +++ b/gui/templates/gui/system/overview.html @@ -0,0 +1,148 @@ +{% extends load_base %} +{% load i18n %} +{% load gui_utils %} + +{% block javascript %} + +{% endblock %} + +{% block base_content %} +
+
+ +
+
+ {% trans "Datacenters" %} +
+ + + + + + + + + + + + + + + + +

{% trans "Total number of virtual datacenters." %}

+
+
+
{{ stats.dcs_total }}
+
+
+
+
+
+ +
+
+ {% trans "Compute nodes" %} +
+ + + + + + + + + + + + + + + + +

{% trans "Total number of compute nodes." %}

+
+
+
{{ stats.nodes_total }}
+
+
+
+
+
+ +
+
+ {% trans "Servers" %} +
+ + + + + + + + + + + + + + + + +

{% trans "Total number of virtual servers." %}

+
+
+
{{ stats.vms_total }}
+
+
+
+
+
+ +
+
+{% endblock %} diff --git a/gui/templates/gui/system/settings.html b/gui/templates/gui/system/settings.html new file mode 100644 index 00000000..ac808b30 --- /dev/null +++ b/gui/templates/gui/system/settings.html @@ -0,0 +1,33 @@ +{% extends load_base %} +{% load i18n %} + +{% block base_content %} +
+
+
+
+
+ {% trans "System configuration" %} +
+
+ +
+

Virtual datacenters and the Danube Cloud system itself can be configured via virtual datacenter settings. There are two kinds of settings:

+
    +
  • + Global DC settings - Global settings have an impact on the behavior of all virtual datacenters and the Danube Cloud system, including objects that cannot be associated with a virtual datacenter (compute nodes). These settings can be modified in the default main virtual datacenter. + +
  • +
  • Local DC settings - Local DC settings affect an actual virtual datacenter and can be modified in every virtual datacenter.
  • +
+
+
+
+
+
+
+{% endblock %} diff --git a/gui/templates/gui/system/update_form.html b/gui/templates/gui/system/update_form.html new file mode 100644 index 00000000..f20c7667 --- /dev/null +++ b/gui/templates/gui/system/update_form.html @@ -0,0 +1,12 @@ +{% load i18n %} +{% csrf_token %} +{% for error in form.non_field_errors %} +
{{ error }}
+{% endfor %} + +{% include "gui/form_field.html" with field=form.version addon='Get latest version' %} +{% include "gui/form_field_checkbox.html" with field=form.force %} +{% if SYSTEM_EDITION == "ee" %} +{% include "gui/form_field.html" with field=form.cert %} +{% include "gui/form_field.html" with field=form.key %} +{% endif %} diff --git a/gui/templates/gui/system/update_modal.html b/gui/templates/gui/system/update_modal.html new file mode 100644 index 00000000..99dd811d --- /dev/null +++ b/gui/templates/gui/system/update_modal.html @@ -0,0 +1,21 @@ +{% load i18n %} + diff --git a/gui/urls.py b/gui/urls.py index f87bbbf1..85de2a91 100644 --- a/gui/urls.py +++ b/gui/urls.py @@ -19,8 +19,9 @@ url(r'^accounts/', include('gui.accounts.urls')), # Profile pages url(r'^accounts/profile/', include('gui.profile.urls')), - # Dashboard pages - url(r'^dashboard/$', 'dashboard', name='dashboard'), + # System pages + url(r'^dashboard/$', RedirectView.as_view(url='/system/overview/', permanent=False)), + url(r'^system/', include('gui.system.urls')), # Datacenter pages url(r'^dc/', include('gui.dc.urls')), # Node pages diff --git a/gui/views.py b/gui/views.py index 4d5d86bb..9a43be81 100644 --- a/gui/views.py +++ b/gui/views.py @@ -1,11 +1,6 @@ -from django.shortcuts import render from django.views.i18n import javascript_catalog from django.views.decorators.http import last_modified from django.utils import timezone -from django.contrib.auth.decorators import login_required - -from gui.decorators import profile_required -from gui.utils import collect_view_data last_modified_date = timezone.now() @@ -17,14 +12,3 @@ def cached_javascript_catalog(request, domain='djangojs', packages=None): https://docs.djangoproject.com/en/1.5/topics/i18n/translation/#note-on-performance """ return javascript_catalog(request, domain, packages) - - -@login_required -@profile_required -def dashboard(request): - """ - Dashboard Page, that is shown as first page after login. - """ - context = collect_view_data(request, 'dashboard') - - return render(request, 'gui/dashboard.html', context) diff --git a/que/handlers.py b/que/handlers.py index 615d964c..5fd6e9d1 100644 --- a/que/handlers.py +++ b/que/handlers.py @@ -103,22 +103,27 @@ def _execute(cmd, stdin=None): # noinspection PyUnusedLocal -def update_command(version, key=None, cert=None, sudo=False): +def update_command(version, key=None, cert=None, force=False, sudo=False, run=False): """Call update script""" from core import settings ssl_key_file = settings.UPDATE_KEY_FILE ssl_cert_file = settings.UPDATE_CERT_FILE update_script = os.path.join(settings.PROJECT_DIR, ERIGONES_UPDATE_SCRIPT) - cmd = [update_script, version] if sudo: - cmd.insert(0, 'sudo') + cmd = ['sudo', update_script, '--esdc-service-restart'] + else: + cmd = [update_script, '--esdc-service-restart'] - if key: + if force: + cmd.append('--force') + + cmd.append(version) + if key: try: - with open(ssl_key_file, 'w+') as f: + with os.fdopen(os.open(ssl_key_file, os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f: f.write(key) except IOError as err: logger.error('Error writing private key to file %s (%s)', ssl_key_file, err) @@ -129,9 +134,8 @@ def update_command(version, key=None, cert=None, sudo=False): cmd.append(ssl_key_file) if cert: - try: - with open(ssl_cert_file, 'w+') as f: + with os.fdopen(os.open(ssl_cert_file, os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f: f.write(cert) except IOError as err: logger.error('Error writing private cert to file %s (%s)', ssl_cert_file, err) @@ -141,7 +145,10 @@ def update_command(version, key=None, cert=None, sudo=False): if os.path.isfile(ssl_cert_file): cmd.append(ssl_cert_file) - return _execute(cmd) + if run: + return _execute(cmd) + else: + return cmd # noinspection PyUnusedLocal @@ -154,10 +161,10 @@ def execute(state, cmd=None, stdin=None): # noinspection PyUnusedLocal @Panel.register -def system_update(state, version=None, key=None, cert=None): - """Panel command that is simple wrapper around update_command performing update""" +def system_update_command(state, version=None, key=None, cert=None, force=None): + """Panel command that is simple wrapper around update_command""" assert version - return update_command(version, key=key, cert=cert) + return update_command(version, key=key, cert=cert, force=force, run=False) # noinspection PyUnusedLocal diff --git a/que/mgmt.py b/que/mgmt.py index 1b75cb41..6f9c3abc 100644 --- a/que/mgmt.py +++ b/que/mgmt.py @@ -177,6 +177,10 @@ def call(self, request, owner_id, args, kwargs=None, meta=None, tt=TT_MGMT, tg=T return t.id, None, None + @classmethod + def get_lock(cls, tidlock_key, **kwargs): + return TaskLock(cls.TIDLOCK_KEY_TEMPLATE % tidlock_key, **kwargs) + @classmethod def clear_cache(cls, cache_result_key): return redis.delete(cls.CACHE_KEY_TEMPLATE % cache_result_key) diff --git a/sio/monitor.py b/sio/monitor.py index 905324f9..eec64ef4 100644 --- a/sio/monitor.py +++ b/sio/monitor.py @@ -2,6 +2,7 @@ from blinker import signal from gevent import sleep +from django.utils.six import itervalues from que.erigonesd import cq from que.utils import user_owner_dc_ids_from_task_id @@ -38,6 +39,10 @@ def _announce_task(event, event_status): if event.get('direct', None): # Send signal to ObjectOwner only users = (int(owner_id),) + elif event.get('broadcast', None): + # Send signal to all active socket.io sessions + from sio.namespaces import ACTIVE_USERS + users = set(session[0] for session in itervalues(ACTIVE_USERS)) else: # Send signal to all affected users users = User.get_super_admin_ids() # SuperAdmins diff --git a/vms/models/node.py b/vms/models/node.py index 7cbb5e84..71ae50e8 100644 --- a/vms/models/node.py +++ b/vms/models/node.py @@ -21,6 +21,9 @@ class Node(_StatusModel, _JsonPickleModel, _UserTasksModel): """ _esysinfo = ('sysinfo', 'diskinfo', 'zpools', 'nictags', 'overlay_rules') _vlan_id = None + _sysinfo_shown = ('Boot Time', 'Manufacturer', 'Product', 'Serial Number', 'SKU Number', 'HW Version', 'HW Family', + 'Datacenter Name', 'VM Capable', 'CPU Type', 'CPU Virtualization', 'CPU Physical Cores', + 'Live Image') ZPOOL = 'zones' DEFAULT_OVERLAY_PORT = 4789 @@ -33,6 +36,7 @@ class Node(_StatusModel, _JsonPickleModel, _UserTasksModel): NODES_ALL_EXPIRES = 300 NICTAGS_ALL_KEY = 'nictag_list' NICTAGS_ALL_EXPIRES = None + SYSTEM_VERSION_EXPIRES = None OFFLINE = 1 ONLINE = 2 @@ -153,9 +157,7 @@ def cpu_sockets(self): def sysinfo(self): """System information displayed in gui/api""" x = self._sysinfo - wanted = ('Boot Time', 'Manufacturer', 'Product', 'Serial Number', 'SKU Number', 'HW Version', 'HW Family', - 'Setup', 'VM Capable', 'CPU Type', 'CPU Virtualization', 'CPU Physical Cores') - return {i: x.get(i, '') for i in wanted} + return {i: x.get(i, '') for i in self._sysinfo_shown} @property def diskinfo(self): @@ -850,7 +852,7 @@ def system_version(self): version = worker_command('system_version', worker, timeout=0.5) or '' if version: - cache.set(self._system_version_key, version) + cache.set(self._system_version_key, version, self.SYSTEM_VERSION_EXPIRES) return version