# Copyright (C) 2014-2020 Codethink Limited # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import logging import os import shutil import tempfile import time import bottle import lorrycontroller class StatusRenderer(object): '''Helper class for rendering service status as JSON/HTML''' def get_queue_status_as_dict(self, statedb): now = statedb.get_current_time() return { 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime(now)), 'run_queue': self.get_run_queue(statedb), } def get_status_as_dict(self, statedb, work_directory): status = self.get_queue_status_as_dict(statedb) status.update({ 'running_queue': statedb.get_running_queue(), 'hosts': self.get_hosts(statedb), 'warning_msg': '', 'max_jobs': self.get_max_jobs(statedb), 'links': True, 'publish_failures': True, }) status.update(self.get_free_disk_space(work_directory)) return status def render_status_as_html(self, template, status): return bottle.template(template, **status) def write_status_as_html(self, template, status, filename, original_static_dir, publish_failures): modified_status = dict(status) modified_status['links'] = False modified_status['publish_failures'] = publish_failures html = self.render_status_as_html(template, modified_status) # We write the file first to a temporary file and then # renaming it into place. If there are any problems, such as # the disk getting full, we won't truncate an existing file. try: temp_filename = self.temp_filename_in_same_dir_as(filename) with open(temp_filename, 'wb') as f: f.write(html.encode("UTF-8")) os.rename(temp_filename, filename) # Copy static files html_dir = os.path.dirname(filename) static_dir = os.path.join(html_dir, 'lc-static') if not os.path.exists(static_dir): os.makedirs(static_dir) static_files = os.listdir(original_static_dir) for file_name in static_files: full_file_name = os.path.join(original_static_dir, file_name) if os.path.isfile(full_file_name): shutil.copy(full_file_name, static_dir) except (OSError, IOError) as e: self.remove_temp_file(temp_filename) status['warning_msg'] = ( 'ERROR WRITING STATUS HTML TO DISK: %s' % str(e)) def temp_filename_in_same_dir_as(self, filename): dirname = os.path.dirname(filename) fd, temp_filename = tempfile.mkstemp(dir=dirname) os.fchmod(fd, 0o644) os.close(fd) return temp_filename def remove_temp_file(self, temp_filename): try: os.remove(temp_filename) except (OSError, IOError): # Ignore a problem with removing. Don't ignore all # exceptions to avoid catching variable names being # mistyped, etc. pass def get_free_disk_space(self, dirname): result = os.statvfs(dirname) free_bytes = result.f_bavail * result.f_bsize return { 'disk_free': free_bytes, 'disk_free_mib': free_bytes // 1024**2, 'disk_free_gib': free_bytes // 1024**3, } def get_run_queue(self, statedb): lorries = statedb.get_all_lorries_info() now = statedb.get_current_time() for lorry in lorries: due = lorry['last_run'] + lorry['interval'] lorry['interval_nice'] = self.format_secs_nicely(lorry['interval']) lorry['due_nice'] = self.format_due_nicely(due, now) return lorries def format_due_nicely(self, due, now): now = int(now) if due <= now: return 'now' else: nice = self.format_secs_nicely(due - now) return 'in %s' % nice def format_secs_nicely(self, secs): if secs <= 0: return 'now' result = [] hours = secs // 3600 secs %= 3600 mins = secs // 60 secs %= 60 if hours > 0: result.append('%d h' % hours) if mins > 0: result.append('%d min' % mins) elif mins > 0: result.append('%d min' % mins) if secs > 0: result.append('%d s' % secs) else: result.append('%d s' % secs) return ' '.join(result) def get_hosts(self, statedb): hosts = [] for host in statedb.get_hosts(): host_info = statedb.get_host_info(host) host_info['ls_interval_nice'] = self.format_secs_nicely( host_info['ls_interval']) ls_due = host_info['ls_last_run'] + host_info['ls_interval'] now = int(statedb.get_current_time()) host_info['ls_due_nice'] = self.format_due_nicely(ls_due, now) hosts.append(host_info) return hosts def get_max_jobs(self, statedb): max_jobs = statedb.get_max_jobs() if max_jobs is None: return 'unlimited' return max_jobs class Status(lorrycontroller.LorryControllerRoute): http_method = 'GET' path = '/1.0/status' def run(self, **kwargs): logging.info('%s %s called', self.http_method, self.path) renderer = StatusRenderer() statedb = self.open_statedb() status = renderer.get_status_as_dict( statedb, self.app_settings['statedb']) renderer.write_status_as_html( self._templates['status'], status, self.app_settings['status-html'], self.app_settings['static-files'], self.app_settings['publish-failures']) return status class StatusHTML(lorrycontroller.LorryControllerRoute): http_method = 'GET' path = '/1.0/status-html' def run(self, **kwargs): logging.info('%s %s called', self.http_method, self.path) renderer = StatusRenderer() statedb = self.open_statedb() status = renderer.get_status_as_dict( statedb, self.app_settings['statedb']) renderer.write_status_as_html( self._templates['status'], status, self.app_settings['status-html'], self.app_settings['static-files'], self.app_settings['publish-failures']) return renderer.render_status_as_html( self._templates['status'], status) class FailuresHTML(lorrycontroller.LorryControllerRoute): http_method = 'GET' path = '/1.0/failures-html' def run(self, **kwargs): logging.info('%s %s called', self.http_method, self.path) renderer = StatusRenderer() with self.open_statedb() as statedb: status = renderer.get_queue_status_as_dict(statedb) return renderer.render_status_as_html( self._templates['failures'], status)