# Copyright (C) 2020-2021 Soleta Networks # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the # Free Software Foundation; either version 3 of the License, or # (at your option) any later version. from flask import ( g, render_template, url_for, flash, redirect, request, jsonify, make_response ) from ogcp.forms.action_forms import ( WOLForm, SetupForm, ClientDetailsForm, ImageDetailsForm, HardwareForm, SessionForm, ImageRestoreForm, ImageCreateForm, SoftwareForm, BootModeForm, RoomForm, DeleteRoomForm, CenterForm, DeleteCenterForm, OgliveForm, GenericForm, SelectClientForm, ImageUpdateForm, ImportClientsForm, ServerForm, DeleteRepositoryForm, RepoForm, FolderForm, CacheForm, ClientMoveForm, RunScriptForm, ImageConfigForm, ImageFetchForm, ServerConfigurationForm, SetRepoForm ) from flask_login import ( current_user, LoginManager, login_user, logout_user, login_required ) from pathlib import Path from ogcp.models import User from ogcp.forms.auth import LoginForm, UserForm, DeleteUserForm, EditUserForm from ogcp.og_server import OGServer, servers from flask_babel import lazy_gettext as _l from flask_babel import gettext, _ from ogcp import app, ogcp_cfg_path from wtforms import StringField import unicodedata import ipaddress import requests import datetime import hashlib import json import os import re from collections import deque FS_CODES = { 0: 'DISK', 1: 'EMPTY', 2: 'CACHE', 6: 'EXT4', 9: 'FAT32', 13: 'NTFS', 18: 'EXFAT', 19: 'LINUX-SWAP' } PART_TYPE_CODES = { 0: 'EMPTY', 1: 'DISK', 5: 'EXTENDED', 7: 'NTFS', 11: 'FAT32', 23: 'HNTFS', 27: 'HFAT32', 39: 'HNTFS-WINRE', 130: 'LINUX-SWAP', 131: 'LINUX', 142: 'LINUX-LVM', 202: 'CACHE', 218: 'DATA', 239: 'EFI', 253: 'LINUX-RAID', 1792: 'NTFS', 9984: 'WIN-RECOV', 33280: 'LINUX-SWAP', 33536: 'LINUX', 36352: 'LINUX-LVM', 51712: 'CACHE', 61184: 'EFI', 64768: 'LINUX-RAID', 65535: 'UNKNOWN' } def get_invalid_image_partition_types(): return ['EMPTY', 'LINUX-SWAP', 'CACHE', 'EFI'] PART_SCHEME_CODES = { 0: 'EMPTY', 1: 'MSDOS', 2: 'GPT' } login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = 'login' class ServerError(Exception): pass class ServerErrorCode(Exception): pass def split_csv(s): return re.split(r'\s*,\s*', s) def normalize_mac(mac): return mac.replace(':', '').replace('-', '').replace('.', '').lower() def is_valid_normalized_mac(mac): if len(mac) != 12: return False if not all(c in '0123456789abcdef' for c in mac): return False return True def prettify_mac(mac): normalized_mac = normalize_mac(mac) if not is_valid_normalized_mac(normalized_mac): return mac return (':'.join(normalized_mac[i:i+2] for i in range(0, 12, 2))).lower() def is_valid_ip(ip): try: ipaddress.ip_address(ip) except: return False return True def remove_accents(text): normalized_text = unicodedata.normalize('NFD', text) return ''.join(c for c in normalized_text if unicodedata.category(c) != 'Mn') def ogserver_down(view): flash(_('Cannot talk to ogserver. Is ogserver down?'), category='error') return redirect(url_for(view)) def ogserver_error(view): flash(_('ogserver replied with a bad HTTP status code'), category='error') return redirect(url_for(view)) def validate_elements(elements, min_len=1, max_len=float('inf')): valid = True if len(elements) < min_len: flash(_('Please, select at least {} element(s)').format(min_len), category='error') valid = not valid elif len(elements) > max_len: flash(_('No more than {} element(s) can be selected for the given action').format(max_len), category='error') valid = not valid return valid def parse_elements(checkboxes_dict): unwanted_elements = ['csrf_token', 'scope-server', 'scope-center', 'scope-room', 'image-server', 'repos-server', 'folder'] elements = set() for key, elements_list in checkboxes_dict.items(): if key not in unwanted_elements: elements.update(elements_list.split(' ')) return elements def client_setup_add_image_names(server, setup_data): r = server.get('/images') if not r: raise ServerError if r.status_code != requests.codes.ok: raise ServerErrorCode images = r.json()['images'] for disk, partitions in setup_data.items(): for p in partitions: if images and p['image'] != 0: image = next((img for img in images if img['id'] == p['image']), None) if image: p['image'] = image['name'] else: p['image'] = "" else: p['image'] = "" def get_client_setup(ip): server = get_server_from_clients([ip]) payload = {'clients': [ip]} r = server.post('/refresh', payload) if not r: raise ServerError if r.status_code != requests.codes.ok: raise ServerErrorCode payload = {'client': [ip]} r = server.get('/client/setup', payload) if not r: raise ServerError if r.status_code != requests.codes.ok: raise ServerErrorCode db_partitions = r.json()['partitions'] res = {} for partition in db_partitions: if partition['partition'] == 0: partition['code'] = PART_SCHEME_CODES.get(partition['code'], 'MSDOS') else: partition['code'] = PART_TYPE_CODES.get(partition['code'], 'EMPTY') partition['filesystem'] = FS_CODES.get(partition['filesystem'], 'EMPTY') disk = partition.get('disk') if disk in res: res[disk].append(partition) else: res[disk] = [partition] return res @app.route('/client/setup', methods=['GET']) @login_required def get_client_setup_json(): ip = parse_elements(request.args.to_dict()) setup = get_client_setup(ip) return jsonify(setup) def get_clients(state_filter=None): responses = multi_request('get', '/clients') clients_list = [] for r in responses: req_clients = r['json']['clients'] for client in req_clients: client['server'] = r['server'] clients_list = clients_list + req_clients clients = {} clients['clients'] = clients_list if state_filter: return filter(clients.items(), lambda c: c.state == state_filter) return clients def get_repository(repository_id, server): try: repositories = get_repositories(server) except ServerError: raise ServerError except ServerErrorCode: raise ServerErrorCode [repository] = [repository for repository in repositories if repository['id'] == repository_id] return repository def get_repositories(server): r = server.get('/repositories') if not r: raise ServerError if r.status_code != requests.codes.ok: raise ServerErrorCode repositories = r.json()['repositories'] return repositories def get_all_repositories(): data = multi_request('get', '/repositories') for item in data: repositories = item['json']['repositories'] sorted_repositories = sorted(repositories, key=lambda repo: repo['name']) item['json']['repositories'] = sorted_repositories return data def parse_scopes_from_tree(tree, scope_type): scopes = [] for scope in tree['scope']: if scope['type'] == scope_type: if 'name' in tree: scope['parent'] = tree['name'] scopes.append(scope) else: scopes += parse_scopes_from_tree(scope, scope_type) return scopes def add_state_and_ips(scope, clients, ips): scope['selected'] = False if 'ip' in scope: filtered_client = filter(lambda x: x['addr']==scope['ip'], clients) client = next(filtered_client, False) if client: scope['state'] = client['state'] scope['link'] = client.get('speed') scope['last_cmd'] = {} scope['last_cmd']['result'] = client.get('last_cmd').get('result') else: scope['state'] = 'off' scope['ip'] = [scope['ip']] scope['selected'] = set(scope['ip']).issubset(ips) else: scope['ip'] = [] for child in scope['scope']: scope['ip'] += add_state_and_ips(child, clients, ips) scope['selected'] = (len(scope['ip']) < 0 and set(scope['ip']).issubset(ips)) return scope['ip'] def remove_disabled_scopes(scopes): scope_list = scopes.get('scope')[:] for scope in scope_list: if scope.get('type') == 'center': if str(scope.get('id')) in current_user.scopes: continue scopes.get('scope').remove(scope) else: remove_disabled_scopes(scope) def multi_request(method, uri, payload=None): responses = [] for server in servers: response = {} if method == 'get': r = server.get(uri, payload) elif method == 'post': r = server.post(uri, payload) else: raise Exception('Invalid method, use get or post') if not r: continue if r.status_code != requests.codes.ok: continue response['server'] = server response['json'] = r.json() responses.append(response) return responses def get_server_from_clients(selected_clients): server = None responses = multi_request('get', '/scopes') for r in responses: server_clients = [c['ip'] for c in parse_scopes_from_tree(r['json'], 'computer')] if all(client in server_clients for client in selected_clients): server = r['server'] break if not server: raise Exception('Selected clients not found in any server') return server def get_server_from_ip_port(str_ip_port): ip, port = str_ip_port.split(':') for s in servers: if s.ip == ip and s.port == int(port): return s raise Exception('Server with address ' + str_ip_port + 'is not configured') def sort_scopes_recursive(ls_scopes): for scope in ls_scopes: if len(scope['scope']) != 0: sorted_ls = sort_scopes_recursive(scope['scope']) scope['scope'] = sorted_ls return sorted(ls_scopes, key=lambda s: s['name'].lower()) def sort_scopes(scopes): all_scopes={} ls = sort_scopes_recursive(scopes['scope']) all_scopes['scope'] = ls return all_scopes def get_scopes(ips=set()): list_scopes = [] responses = multi_request('get', '/scopes') for r in responses: scopes = r['json'] server_scope = {} server_scope['name'] = r['server'].name server_scope['type'] = "server" server_scope['server_ip_port'] = (r['server'].ip + ":" + str(r['server'].port)) server_scope.update(scopes) list_scopes.append(server_scope) all_scopes = {'scope': list_scopes} all_scopes = sort_scopes(all_scopes) if not current_user.admin and current_user.scopes: remove_disabled_scopes(all_scopes) clients = get_clients() add_state_and_ips(all_scopes, clients['clients'], ips) return all_scopes, clients def hash_password(pwd): sha = hashlib.sha512() sha.update(pwd.encode()) pwd_hash = sha.hexdigest() return pwd_hash def get_cache_info(clients_info, storage_data, images_data, client_images): for client_info in clients_info: ip = client_info['ip'] cache_size = client_info['cache_size'] * 1024 used_cache = client_info['used_cache'] free_cache = client_info['free_cache'] for image_info in client_info['images']: image_name = image_info['name'] checksum = image_info['checksum'] img_identifier = f'{image_name}.{checksum}' if ip in client_images: client_images[ip].append(img_identifier) else: client_images[ip] = [img_identifier] if img_identifier in images_data: images_data[img_identifier]['clients'].append(ip) else: images_data[img_identifier] = { 'clients': [ip], 'size': int(image_info['size']), 'name': image_name, 'checksum': checksum } storage_data[ip] = {'used': used_cache, 'free': free_cache} def authenticate_user(username, pwd): for user in app.config['USERS']: if user.get("USER") == username: if user.get("PASS") == pwd: return user else: flash(_('Incorrect password')) return None flash(_('Incorrect user name')) return None def get_user(username): for user in app.config['USERS']: if user.get("USER") == username: return user return None intervals = ( (_l('days'), 86400), # 60 * 60 * 24 (_l('hours'), 3600), # 60 * 60 (_l('minutes'), 60), ) def display_time(seconds): result = [] for name, count in intervals: value = seconds // count if value: seconds -= value * count result.append("{} {}".format(value, name)) return ', '.join(result) @login_manager.user_loader def load_user(username): user_dict = get_user(username) if not user_dict: return None user = User(username, user_dict.get('SCOPES'), user_dict.get('ADMIN'), user_dict.get('PERMISSIONS', {})) return user @app.errorhandler(404) def page_not_found(error): return render_template('error.html', message=error), 404 @app.errorhandler(500) def server_error(error): return render_template('error.html', message=error), 500 def image_modified_date_from_str(image): return datetime.datetime.strptime(image['modified'], '%a %b %d %H:%M:%S %Y') @app.route('/') def index(): if not current_user.is_authenticated: return redirect(url_for('login')) images_response = multi_request('get', '/images') dashboard_servers = {} for i in images_response: server_name = i['server'].name server_id = i['server'].id images = i['json']['images'] images.sort(key=image_modified_date_from_str, reverse=True) disk = i['json']['disk'] if server_name not in dashboard_servers: dashboard_servers[server_id] = {'name': server_name} dashboard_servers[server_id]['images'] = images dashboard_servers[server_id]['disk'] = disk oglive_list = multi_request('get', '/oglive/list') for i in oglive_list: server_id = i['server'].id dashboard_servers[server_id]['oglive_list'] = i['json'] all_stats = multi_request('get', '/stats') for server in servers: active = False stat = None for i in all_stats: if i['server'].id == server.id: active = True stat = i break if active: server_id = stat['server'].id dashboard_servers[server_id]['online'] = True stats = stat['json'] dashboard_servers[server_id]['stats'] = stats timestamp = datetime.datetime.fromtimestamp(stats.get('time').get('now')) now = timestamp.strftime('%Y-%m-%d %H:%M:%S') boot = display_time(stats.get('time').get('boot')) start = display_time(stats.get('time').get('start')) time_dict = {'now': now, 'boot': boot, 'start': start} dashboard_servers[server_id]['time_dict'] = time_dict else: timestamp = datetime.datetime.today() dashboard_servers[server.id] = {} dashboard_servers[server.id]['online'] = False dashboard_servers[server.id]['name'] = server.name dashboard_servers[server.id]['time_dict'] = {'now': None, 'boot': 'Offline', 'start': None} dashboard_servers[server.id]['clients'] = [] dashboard_servers[server.id]['images'] = [] dashboard_servers[server.id]['disk'] = {'total': 0, 'free':0} dashboard_servers[server.id]['stats'] = {'memory': {'free': 0, 'size':0}, 'swap': ''} dashboard_servers[server.id]['oglive_list'] = [] clients_response = multi_request('get', '/clients') for i in clients_response: server_id = i['server'].id dashboard_servers[server_id]['clients'] = i['json']['clients'] now = timestamp.strftime('%Y-%m-%d %H:%M:%S') return render_template('dashboard.html', servers=dashboard_servers, now=now, colsize="6") @app.route('/login', methods=['GET', 'POST']) def login(): form = LoginForm(request.form) if request.method == 'POST' and form.validate(): form_user = request.form['user'] pwd = request.form['pwd'] pwd_hash = hash_password(pwd) user_dict = authenticate_user(form_user, pwd_hash) if not user_dict: return render_template('auth/login.html', form=form) user = User(form_user, user_dict.get('SCOPES'), user_dict.get('ADMIN'), user_dict.get('PERMISSIONS', {})) login_user(user) return redirect(url_for('index')) return render_template('auth/login.html', form=LoginForm()) @app.route("/logout") @login_required def logout(): logout_user() return redirect(url_for('index')) @app.route('/scopes/status') @login_required def scopes_status(): scopes, _clients = get_scopes() return jsonify(scopes) @app.route('/client/mac', methods=['GET']) @login_required def get_client_mac(): ip = parse_elements(request.args.to_dict()) payload = {'client': list(ip)} server = get_server_from_clients(list(ip)) resp = server.get('/client/info', payload) client_info = resp.json() mac = client_info.get('mac') pretty_mac = prettify_mac(mac) return jsonify(pretty_mac) def get_server_data_from_scopes(scopes, clients): servers_data = {} ip_to_name = {ip: c['name'] for c in parse_scopes_from_tree(scopes, 'computer') for ip in c['ip']} for client in clients['clients']: if not client['addr'] in ip_to_name: continue client['name'] = ip_to_name[client['addr']] server_id = client['server'].id if server_id not in servers_data: servers_data[server_id] = {'clients': []} for server in servers: if server.id == server_id: servers_data[server_id]['name'] = server.name servers_data[server_id]['clients'].append(client) for server_id in servers_data: servers_data[server_id]['clients'].sort(key=lambda x: x['name']) return servers_data @app.route('/client/list', methods=['GET']) @login_required def client_list(): scopes, clients = get_scopes() servers_data = get_server_data_from_scopes(scopes, clients) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('client_list.html', servers_data=servers_data, selected_clients=selected_clients, scopes=scopes) @app.route('/scopes/') @login_required def scopes(): scopes, clients = get_scopes() return render_template('scopes.html', scopes=scopes, clients=clients) @app.route('/action/poweroff', methods=['GET', 'POST']) @login_required def action_poweroff(): form = GenericForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') if not validate_elements(ips): return redirect(url_for('commands')) payload = {'clients': ips} server = get_server_from_clients(ips) r = server.post('/poweroff', payload) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') flash(_('Client powered off successfully'), category='info') return redirect(url_for('commands')) else: ips = parse_elements(request.args.to_dict()) form.ips.data = " ".join(ips) if validate_elements(ips): scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/poweroff.html', form=form, selected_clients=selected_clients, scopes=scopes) else: return redirect(url_for('commands')) @app.route('/action/wol', methods=['GET', 'POST']) @login_required def action_wol(): form = WOLForm(request.form) if request.method == 'POST' and form.validate(): wol_type = form.wol_type.data ips = form.ips.data.split(' ') payload = {'type': wol_type, 'clients': ips} server = get_server_from_clients(ips) server.post('/wol', payload) flash(_('Wake On Lan request sent successfully'), category='info') return redirect(url_for('commands')) else: ips = parse_elements(request.args.to_dict()) form.ips.data = " ".join(ips) if validate_elements(ips, min_len=1): scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/wol.html', form=form, selected_clients=selected_clients, scopes=scopes) else: return redirect(url_for('commands')) def get_common_disk_data(ips): disk_data = {} for ip in ips: setup_data = get_client_setup(ip) for disk, partitions in setup_data.items(): for p in partitions: if p.get('partition') != 0: continue if disk in disk_data: disk_data[disk]['common_size'] = min(disk_data[disk]['common_size'], p.get('size')) disk_data[disk]['inventory'].setdefault(p.get('size'), []).append(ip) else: disk_data[disk] = { 'common_size': p.get('size'), 'excluded': [], 'inventory': {p.get('size'): [ip]} } break for disk_id in disk_data: disk_ips = [] for disk_size in disk_data[disk_id]['inventory']: disk_ips.extend(disk_data[disk_id]['inventory'][disk_size]) disk_data[disk_id]['excluded'] = [ip for ip in ips if ip not in disk_ips] return disk_data @app.route('/action/setup/select', methods=['GET']) @login_required def action_setup_select(): args = request.args.copy() ips = parse_elements(args.to_dict()) if not validate_elements(ips): return redirect(url_for('commands')) if len(ips) == 1: ip = list(ips)[0] return redirect(url_for('action_setup_show', ip=ip)) form = SelectClientForm() form.ips.data = " ".join(ips) server = get_server_from_clients(list(ips)) client_choices = [] for ip in ips: r = server.get('/client/info', payload={'client': [ip]}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') client_name = r.json()['name'] client_choices.append((ip, f"{client_name} ({ip})")) form.selected_client.choices = client_choices common_disk_data = get_common_disk_data(ips) scopes, unused = get_scopes(ips) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/select_client.html', common_disk_data=common_disk_data, selected_clients=selected_clients, form=form, scopes=scopes) @app.route('/action/setup', methods=['GET']) @login_required def action_setup_show(): args = request.args.copy() disk_size = None if args.get('ip'): ips = {args['ip']} ips_str = base_client = args['ip'] else: ips_str = args['ips'] ips = set(args['ips'].split(' ')) base_client = args['selected_client'] try: setup_data = get_client_setup(base_client) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') if not setup_data: flash(_('Partition information is not available. Boot client in ogLive mode to obtain it'), category='error') return redirect(url_for('commands')) selected_disk = 1 common_disk_data = get_common_disk_data(ips) # Use common disk size for disk in setup_data: setup_data[disk][0]['size'] = common_disk_data[disk]['common_size'] excluded_client_disks = {} for disk in common_disk_data: excluded_client_disks[disk] = common_disk_data[disk]['excluded'] form = SetupForm() form.ips.data = ips_str form.disk.choices = [(disk, disk) for disk in setup_data] form.disk.data = selected_disk # If partition table is empty, set MSDOS form.disk_type.data = setup_data[selected_disk][0]['code'] or 1 scopes, _clients = get_scopes(ips) return render_template('actions/setup.html', excluded_client_disks=excluded_client_disks, selected_disk=selected_disk, setup_data=setup_data, disk_form=form, scopes=scopes) @app.route('/action/setup', methods=['POST']) @login_required def action_setup_modify(): form = SetupForm(request.form) MIN_EFI_SIZE = 500 if form.validate(): cache_count = 0 for partition in form.partitions: if partition.part_type.data == 'CACHE': cache_count += 1 if partition.part_type.data == 'EFI' and partition.fs.data != 'FAT32': flash(_('The EFI partition requires a FAT32 filesystem'), category='error') return redirect(url_for('commands')) if partition.part_type.data == 'EFI' and partition.size.data < MIN_EFI_SIZE: flash(_(f'The EFI partition requires a size of {MIN_EFI_SIZE}MiB or higher'), category='error') return redirect(url_for('commands')) if partition.size.data <= 0: flash(_('Partitions can\'t have a size of zero or lower'), category='error') return redirect(url_for('commands')) if cache_count > 1: flash(_(f'More than one cache partition is not supported'), category='error') return redirect(url_for('commands')) ips = form.ips.data.split(' ') payload = {'clients': ips, 'disk': str(form.disk.data), 'type': str(form.disk_type.data), 'cache': str(0), 'cache_size': str(0), 'partition_setup': []} partition_index = 0 for partition in form.partitions: partition_index += 1 partition_setup = {'partition': str(partition_index), 'code': str(partition.part_type.data), 'filesystem': str(partition.fs.data), 'size': str(partition.size.data * 1024), 'format': '1'} payload['partition_setup'].append(partition_setup) if partition.part_type.data == 'CACHE': payload['cache'] = '1' payload['cache_size'] = str(partition.size.data * 1024) for partition_index in range(len(form.partitions) + 1, 5): empty_part = { 'partition': str(partition_index), 'code': 'EMPTY', 'filesystem': 'EMPTY', 'size': '0', 'format': '1', } payload['partition_setup'].append(empty_part) server = get_server_from_clients(list(ips)) r = server.post('/setup', payload=payload) if not r: return ogserver_down('commands') if r.status_code == requests.codes.ok: return redirect(url_for('commands')) flash(_(f'Invalid setup form'), category='error') return redirect(url_for('commands')) def search_image(images_list, image_id): for image in images_list: if image['id'] == image_id: return image return False def filter_images_allowed_in_center(server, images, center_id): res = [] for image in images: r = server.get('/image/restrict', {'image': image['id']}) if not r: raise ServerError if r.status_code != requests.codes.ok: raise ServerErrorCode allowed_scopes = r.json().get('scopes') if not allowed_scopes or center_id in allowed_scopes: res.append(image) return res def get_images_from_repo(server, repo_id): r = server.get('/images') if not r: raise ServerError if r.status_code != requests.codes.ok: raise ServerErrorCode images = r.json()['images'] res=[] for image in images: if image['repo_id'] == repo_id: res.append(image) return res def get_image_key(image): return image['name'] def sort_images(images): images.sort(key=get_image_key, reverse=False) def get_clients_repo(server, ips): repo_id=None for ip in ips: r = server.get('/client/info', payload={'client': [ip]}) if not r: raise ServerError if r.status_code != requests.codes.ok: raise ServerErrorCode repo_id_aux = r.json()['repo_id'] if repo_id is None: repo_id = repo_id_aux elif repo_id_aux != repo_id: return None return repo_id def image_fits_in_cache(server, clients_info, image): image_size = int(image.get('size', '0')) image_checksum = image.get('checksum', '') if not (image_size and image_checksum): return True err_report = "" for client_info in clients_info: ip = client_info['ip'] cache_size = client_info['cache_size'] * 1024 has_image = False; for image_info in client_info['images']: if image_info['checksum'] == image_checksum: has_image = True if has_image: continue free_cache = client_info['free_cache'] used_cache = client_info['used_cache'] if used_cache == 0 and free_cache == 0: err_report += f'{ip} has no cache partition
' continue if free_cache < image_size: missing_cache = image_size - free_cache err_report += f'{ip} requires {(missing_cache / (1024 ** 3)):.3f} more free GiB
' if err_report: flash(f'{err_report}', category='error') return False return True class PartitionCollection: def __init__(self): self.partition_list = [] self.clients = [] self.empty_scheme = False def __len__(self): return len(self.partition_list) def has_empty_scheme(self): return self.empty_scheme def register_partition_setup(self, partitions, client): idx = None self.empty_scheme = self.empty_scheme or not partitions for index, p in enumerate(self.partition_list): if p == partitions: idx = index self.clients[idx].append(client) break else: idx = len(self.partition_list) self.partition_list.append(partitions) self.clients.append([client]) return idx def get_partition_setup(self, idx): return self.partition_list[idx] def get_clients(self, idx): return self.clients[idx] @app.route('/action/image/restore', methods=['GET', 'POST']) @login_required def action_image_restore(): form = ImageRestoreForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') disk, partition, part_size, has_cache = form.partition.data.split(' ') requires_cache = form.method.data == 'TIPTORRENT' or form.method.data == 'UNICAST' if has_cache == 'False' and requires_cache: flash(_(f'Cannot restore image using {form.method.data} on a client without cache'), category='error') return redirect(url_for('commands')) image_id = form.image.data server = get_server_from_clients(ips) r = server.get('/images') if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') images_list = r.json()['images'] image = search_image(images_list, int(image_id)) if not image: flash(_(f'Image to restore was not found'), category='error') return redirect(url_for('commands')) image_datasize = int(image['datasize']) part_size = int(part_size) * 1024 if image_datasize and image_datasize > part_size: flash(_(f'The image size is bigger than the target partition'), category='error') return redirect(url_for('commands')) r = server.get('/cache/list', payload={'clients': ips}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') clients_info = r.json()['clients'] if requires_cache and not image_fits_in_cache(server, clients_info, image): return redirect(url_for('commands')) payload = {'disk': disk, 'partition': partition, 'name': image['name'], 'repository_id': image['repo_id'], 'clients': ips, 'type': form.method.data, 'profile': str(image['software_id']), 'id': str(image['id'])} server.post('/image/restore', payload) if not r: return ogserver_down('commands') if r.status_code == requests.codes.ok: flash(_(f'Image restore command sent sucessfully'), category='info') else: flash(_(f'There was a problem sending the image restore command'), category='error') return redirect(url_for('commands')) else: params = request.args.to_dict() center_id = int(params.get('scope-center')) ips = parse_elements(params) if not validate_elements(ips): return redirect(url_for('commands')) form.ips.data = ' '.join(ips) server = get_server_from_clients(ips) try: repo_id = get_clients_repo(server, ips) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') if repo_id is None: flash(_(f'Computers have different repos assigned'), category='error') return redirect(url_for('commands')) try: images = get_images_from_repo(server, repo_id) if not images: flash(_(f'Computer(s) assigned to a repo with no images'), category='error') return redirect(url_for('commands')) images = filter_images_allowed_in_center(server, images, center_id) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') sort_images(images) for image in images: form.image.choices.append((image['id'], image['name'])) invalid_part_types = get_invalid_image_partition_types() part_collection = PartitionCollection() for ip in ips: r = server.get('/client/setup', payload={'client': [ip]}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') has_cache = False partitions = r.json()['partitions'] parts = [] for partition in partitions: disk_id = partition['disk'] part_id = partition['partition'] if part_id == 0: # This is the disk data, not a partition. continue if disk_id != 1: continue part_code = partition['code'] part_type = PART_TYPE_CODES.get(int(part_code), 'UNKNOWN') if part_type == 'CACHE': has_cache = True if part_type in invalid_part_types: continue filesystem = partition['filesystem'] fs_type = FS_CODES.get(filesystem, 'UNKNOWN') part_size = partition['size'] parts.append((disk_id, part_id, part_type, fs_type, part_size)) part_collection.register_partition_setup(parts, ip) scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) if len(part_collection) > 1 or part_collection.has_empty_scheme(): return render_template('actions/partition_report.html', selected_clients=selected_clients, part_data=part_collection, scopes=scopes) reference_patitioning = part_collection.get_partition_setup(0) if not reference_patitioning: flash(_(f'No valid partition found'), category='error') return redirect(url_for('commands')) for disk_id, part_id, part_type, fs_type, part_size in reference_patitioning: form.partition.choices.append( (f"{disk_id} {part_id} {part_size} {has_cache}", f"Disk {disk_id} | Partition {part_id} " f"| {part_type} " f"{fs_type}") ) return render_template('actions/image_restore.html', form=form, selected_clients=selected_clients, scopes=scopes) @app.route('/action/hardware', methods=['GET', 'POST']) @login_required def action_hardware(): form = HardwareForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') server = get_server_from_clients(ips) r = server.post('/hardware', payload={'clients': ips}) if not r: return ogserver_down('commands') if r.status_code == requests.codes.ok: flash(_(f'Hardware inventory command has been sent'), category='info') else: flash(_(f'There was a problem sending the hardware inventory command'), category='error') return redirect(url_for('commands')) else: ips = parse_elements(request.args.to_dict()) scopes, _clients = get_scopes(ips) if not validate_elements(ips, max_len=1): return redirect(url_for('commands')) form.ips.data = ' '.join(ips) server = get_server_from_clients(ips) r = server.get('/hardware', payload={'client': list(ips)}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') hardware = r.json()['hardware'] return render_template('actions/hardware.html', form=form, hardware=hardware, scopes=scopes) @app.route('/action/software', methods=['GET', 'POST']) @login_required def action_software(): form = SoftwareForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') disk, partition = form.os.data.split(' ') server = get_server_from_clients(ips) if form.view.data: r = server.get('/software', payload={'client': ips, 'disk': int(disk), 'partition': int(partition)}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') software = r.json()['software'] scopes, clients = get_scopes(set(ips)) return render_template('actions/software_list.html', software=software, form=form, scopes=scopes) elif form.update.data: r = server.post('/software', payload={'clients': ips, 'disk': disk, 'partition': partition}) if not r: return ogserver_down('commands') if r.status_code == requests.codes.ok: flash(_('Software profile request sent successfully'), category='info') else: flash(_('Error processing software profile request: ({})').format(r.status), category='error') else: flash(_('Error processing software profile form'), category='error') return redirect(url_for('commands')) else: ips = parse_elements(request.args.to_dict()) scopes, clients = get_scopes(set(ips)) if not validate_elements(ips, max_len=1): return redirect(url_for('commands')) form.ips.data = ' '.join(ips) server = get_server_from_clients(ips) r = server.get('/client/setup', payload={'client': list(ips)}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') if not r.json()['partitions']: flash(_('Software inventory is not available. Boot client in ogLive mode to obtain it'), category='error') return redirect(url_for('commands')) for part in r.json()['partitions'][1:]: form.os.choices.append( (f"{part.get('disk')} {part.get('partition')}", f"Disk {part.get('disk')} | Partition {part.get('partition')} " f"| {PART_TYPE_CODES.get(part.get('code'), 'UNKNOWN')} " f"{FS_CODES.get(part.get('filesystem'), 'UNKNOWN')}") ) return render_template('actions/software.html', form=form, scopes=scopes) @app.route('/action/session', methods=['GET', 'POST']) @login_required def action_session(): form = SessionForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') disk, partition, os_name = form.os.data.split(' ', 2) server = get_server_from_clients(list(ips)) r = server.get('/session', payload={'clients': ips}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') sessions = r.json()['sessions'] if not sessions: flash(_('ogServer returned an empty session list'), category='error') return redirect(url_for('commands')) valid_ips = [] excluded_ips = [] for client_sessions in sessions: ip = client_sessions['addr'] os_found = False for p_data in client_sessions['partitions']: if p_data['disk'] == int(disk) and p_data['partition'] == int(partition) and p_data['name'] == os_name: os_found = True break if os_found: valid_ips.append(ip) else: excluded_ips.append(ip) r = server.post('/session', payload={'clients': valid_ips, 'disk': str(disk), 'partition': str(partition)}) if not r: return ogserver_down('commands') if r.status_code == requests.codes.ok: if excluded_ips: flash('The following clients didn\'t match the boot configuration: ' + str(excluded_ips)) return redirect(url_for('commands')) return make_response("400 Bad Request", 400) else: ips = parse_elements(request.args.to_dict()) ips_list = list(ips) if not validate_elements(ips): return redirect(url_for('commands')) server = get_server_from_clients(ips_list) form.ips.data = ' '.join(ips_list) r = server.get('/session', payload={'clients': ips_list}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') sessions = r.json()['sessions'] if not sessions: flash(_('ogServer returned an empty session list'), category='error') return redirect(url_for('commands')) os_groups = {} for client_sessions in sessions: ip = client_sessions['addr'] for p_data in client_sessions['partitions']: if p_data['name'] == 'unknown': continue item_key = f"{p_data['disk']} {p_data['partition']} {p_data['name']}" if item_key in os_groups: os_groups[item_key].append(ip) else: os_groups[item_key] = [ip] choice = (item_key, f"{p_data['name']} (Disk:{p_data['disk']}, Partition:{p_data['partition']})") form.os.choices.append(choice) scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/session.html', form=form, selected_clients=selected_clients, scopes=scopes, os_groups=os_groups) @app.route('/action/cache', methods=['GET', 'POST']) @login_required def action_client_cache(): form = CacheForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') server = get_server_from_clients(list(ips)) client_list = [] image_list = [] for entry in form.images.entries: if not entry.selected.data: continue image_list.append(entry.image_name.data) for client_ip in entry.clients.data.split(' '): if not client_ip in client_list: client_list.append(client_ip) if not image_list: flash(_(f'No selected images to delete'), category='error') return redirect(url_for('commands')) r = server.post('/cache/delete', payload={'clients': client_list, 'images': image_list}) if not r: return ogserver_down('commands') if r.status_code == requests.codes.ok: flash(_('Cache delete request sent successfully'), category='info') else: flash(_(f'Invalid cache delete form'), category='error') return redirect(url_for('commands')) else: ips = parse_elements(request.args.to_dict()) ips_list = list(ips) if not validate_elements(ips): return redirect(url_for('commands')) form.ips.data = ' '.join(ips_list) server = get_server_from_clients(ips_list) r = server.get('/cache/list', payload={'clients': ips_list}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') clients_info = r.json()['clients'] if not clients_info: flash(_('ogServer returned an empty client list'), category='error') return redirect(url_for('commands')) storage_data = {} images_data = {} client_images = {} get_cache_info(clients_info, storage_data, images_data, client_images) for img_identifier in images_data: image_data = images_data[img_identifier] checkbox_entry = form.images.append_entry() checkbox_entry.selected.label.text = img_identifier checkbox_entry.image_name.data = image_data['name'] checkbox_entry.clients.data = ' '.join(image_data['clients']) scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/cache.html', form=form, selected_clients=selected_clients, scopes=scopes, images_data=images_data, storage_data=storage_data, client_images=client_images) @app.route('/action/cache/fetch', methods=['GET', 'POST']) @login_required def action_image_fetch(): form = ImageFetchForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') server = get_server_from_clients(list(ips)) image_id = form.image.data server = get_server_from_clients(ips) r = server.get('/images') if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') images_list = r.json()['images'] image = search_image(images_list, int(image_id)) if not image: flash(_(f'Image to fetch was not found'), category='error') return redirect(url_for('commands')) r = server.get('/cache/list', payload={'clients': ips}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') clients_info = r.json()['clients'] if not image_fits_in_cache(server, clients_info, image): return redirect(url_for('commands')) payload = {'clients': ips, 'repository_id': image['repo_id'], 'type': form.method.data, 'image': image['name']} r = server.post('/cache/fetch', payload=payload) if not r: return ogserver_down('commands') if r.status_code == requests.codes.ok: flash(_('Cache fetch request sent successfully'), category='info') else: flash(_(f'Invalid cache fetch form'), category='error') return redirect(url_for('commands')) else: params = request.args.to_dict() ips = parse_elements(params) center_id = int(params.get('scope-center')) if not validate_elements(ips): return redirect(url_for('commands')) ips_list = list(ips) form.ips.data = ' '.join(ips_list) server = get_server_from_clients(ips) try: repo_id = get_clients_repo(server, ips) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') if repo_id is None: flash(_(f'Computers have different repos assigned'), category='error') return redirect(url_for('commands')) try: images = get_images_from_repo(server, repo_id) if not images: flash(_(f'Computer(s) assigned to a repo with no images'), category='error') return redirect(url_for('commands')) images = filter_images_allowed_in_center(server, images, center_id) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') sort_images(images) for image in images: form.image.choices.append((image['id'], image['name'])) scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/cache_fetch.html', form=form, selected_clients=selected_clients, scopes=scopes) @app.route('/action/client/info', methods=['GET']) @login_required def action_client_info(): form = ClientDetailsForm() ips = parse_elements(request.args.to_dict()) if not validate_elements(ips, max_len=1): return redirect(url_for('commands')) server = get_server_from_clients(list(ips)) payload = {'client': list(ips)} r = server.get('/client/info', payload) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') db_client = r.json() form.name.data = db_client['name'] form.name.render_kw = {'readonly': True} form.ip.data = db_client['ip'] form.ip.render_kw = {'readonly': True} form.mac.data = prettify_mac(db_client['mac']) form.mac.render_kw = {'readonly': True} form.serial_number.data = db_client['serial_number'] form.serial_number.render_kw = {'readonly': True} form.livedir.choices = [(db_client['livedir'], db_client['livedir'])] form.livedir.render_kw = {'readonly': True} form.remote.data = db_client['remote'] form.remote.render_kw = {'readonly': True} form.maintenance.data = db_client['maintenance'] form.maintenance.render_kw = {'readonly': True} try: repositories = get_repositories(server) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') form.repo.choices = [(repo["id"], repo["name"]) for repo in repositories if db_client['repo_id'] == repo["id"]] form.repo.render_kw = {'readonly': True} form.room.data = db_client['room'] form.room.render_kw = {'readonly': True} form.boot.choices = [(db_client['boot'], db_client['boot'])] form.boot.render_kw = {'readonly': True} r = server.get('/scopes') if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') rooms = parse_scopes_from_tree(r.json(), 'room') rooms = [(room['id'], room['name']) for room in rooms if room['id'] == int(db_client['room'])] form.room.choices = list(rooms) form.submit.render_kw = {"style": "visibility:hidden;"} ip = list(ips)[0] try: setup_data = get_client_setup(ip) client_setup_add_image_names(server, setup_data) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') disk_form = SetupForm() selected_disk = 1 disk_form.disk.choices = [(disk, disk) for disk in setup_data] disk_form.disk.data = selected_disk r = server.get('/cache/list', payload={'clients': [ip]}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') clients_info = r.json()['clients'] if not clients_info: flash(_('ogServer returned an empty client list'), category='error') return redirect(url_for('commands')) storage_data = {} images_data = {} client_images = {} get_cache_info(clients_info, storage_data, images_data, client_images) r = server.get('/efi', payload={'clients': [ip]}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') efi_data = r.json()['clients'][0] scopes, clients = get_scopes(set(ips)) return render_template('actions/client_details.html', form=form, parent="commands.html", scopes=scopes, setup_data=setup_data, disk_form=disk_form, selected_disk=selected_disk, images_data=images_data, storage_data=storage_data, client_images=client_images, efi_data=efi_data) @app.route('/action/client/update', methods=['GET', 'POST']) @login_required def action_client_update(): form = ClientDetailsForm(request.form) if request.method == 'POST': if not is_valid_ip(form.ip.data): flash(_('Invalid IP address'), category='error') return redirect(url_for("scopes")) mac_address = normalize_mac(form.mac.data) if not is_valid_normalized_mac(mac_address): flash(_('Invalid MAC address'), category='error') return redirect(url_for("scopes")) payload = {"ip": form.ip.data, "id": int(form.client_id.data), "serial_number": form.serial_number.data, "netdriver": "generic", "maintenance": form.maintenance.data, "netiface": "eth0", "repo_id": int(form.repo.data), "netmask": "0", "remote": form.remote.data, "room": int(form.room.data), "name": form.name.data, "boot": form.boot.data, "mac": mac_address } server = get_server_from_ip_port(form.server.data) r = server.post('/client/update', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('ogServer: error updating client'), category='error') else: flash(_('Client updated successfully'), category='info') return redirect(url_for("scopes")) else: ips = parse_elements(request.args.to_dict()) if not validate_elements(ips, max_len=1): return redirect(url_for('scopes')) server = get_server_from_clients(list(ips)) scopes, clients = get_scopes() payload = {'client': list(ips)} r = server.get('/client/info', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') db_client = r.json() form.server.data = "{0}:{1}".format(server.ip, server.port) form.client_id.data = db_client['id'] form.ip.data = db_client['ip'] form.name.data = db_client['name'] form.mac.data = prettify_mac(db_client['mac']) form.serial_number.data = db_client['serial_number'] form.livedir.render_kw = {'readonly': True} form.remote.data = db_client['remote'] form.maintenance.data = db_client['maintenance'] current_mode = db_client['boot'] r = server.get('/mode') if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') available_modes = [(current_mode, current_mode)] available_modes.extend([(mode, mode) for mode in r.json()['modes'] if mode != current_mode]) form.boot.choices = list(available_modes) form.boot.render_kw = {'readonly': True} r = server.get('/scopes') if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') room_id = db_client['room'] rooms = parse_scopes_from_tree(r.json(), 'room') rooms = [(room['id'], room['name']) for room in rooms if room['id'] == int(room_id)] form.room.choices = list(rooms) form.room.render_kw = {'readonly': True} try: repositories = get_repositories(server) except ServerError: return ogserver_down('scopes') except ServerErrorCode: return ogserver_error('scopes') form.repo.choices = [(repo["id"], repo["name"]) for repo in repositories if db_client['repo_id'] == repo["id"]] form.repo.choices.extend([(repo["id"], repo["name"]) for repo in repositories if db_client['repo_id'] != repo["id"]]) ip = list(ips)[0] try: setup_data = get_client_setup(ip) client_setup_add_image_names(server, setup_data) except ServerError: return ogserver_down('scopes') except ServerErrorCode: return ogserver_error('scopes') disk_form = SetupForm() selected_disk = 1 disk_form.disk.choices = [(disk, disk) for disk in setup_data] disk_form.disk.data = selected_disk form.submit.render_kw = {"formaction": url_for('action_client_update')} return render_template('actions/client_details.html', form=form, setup_data=setup_data, disk_form=disk_form, selected_disk=selected_disk, parent="scopes.html", scopes=scopes) def find_folder(folder_id, scopes): scopes = deque([scopes['scope']]) while scopes: scope_ls = scopes.popleft() if not scope_ls: continue else: for scope in scope_ls: if scope['type'] == 'folder' and scope['id'] == folder_id: return scope else: scopes.append(scope['scope']) return None @app.route('/action/folder/delete', methods=['GET', 'POST']) @login_required def action_folder_delete(): form = FolderForm(request.form) if request.method == 'POST': payload = {"id": int(form.folder_id.data)} server = get_server_from_ip_port(form.server.data) r = server.post('/folder/delete', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('ogServer: error deleting folder'), category='error') else: flash(_('Folder deleted successfully'), category='info') return redirect(url_for("scopes")) else: params = request.args.to_dict() folder_id = params.get('folder') if not folder_id: flash(_('Please, select a folder'), category='error') return redirect(url_for('scopes')) form.folder_id.data = folder_id form.server.data = params['scope-server'] form.submit.render_kw = {"formaction": url_for('action_folder_delete')} scopes, unused = get_scopes() ancestors, children = get_scope_context(int(folder_id), 'folder', scopes) del form.name return render_template('actions/folder_delete.html', form=form, parent="scopes.html", scopes=scopes, ancestors=ancestors, children=children) @app.route('/action/folder/update', methods=['GET','POST']) def action_folder_update(): form = FolderForm(request.form) if request.method == 'POST': payload = {'name': form.name.data, 'id': int(form.folder_id.data)} server = get_server_from_ip_port(form.server.data) r = server.post('/folder/update', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('ogServer: error updating folder'), category='error') else: flash(_('Folder updated successfully'), category='info') return redirect(url_for("scopes")) else: params = request.args.to_dict() if not 'folder' in params: flash(_('Please, select a folder to modify'), category='error') return redirect(url_for('scopes')) folder_id = int(params.get('folder')) scopes, clients = get_scopes() folder = find_element_scope(folder_id, 'folder', scopes) form.server.data = params['scope-server'] form.name.data = folder['name'] form.folder_id.data = folder_id form.submit.render_kw = {"formaction": url_for('action_folder_update')} return render_template('actions/folder_update.html', form=form, parent="scopes.html", scopes=scopes) @app.route('/action/folder/add', methods=['GET']) @login_required def action_folder_add(): form = FolderForm() params = request.args.to_dict() room = params.get('scope-room') center = params.get('scope-center') if room and center: flash(_('Please, select either a room or a center'), category='error') return redirect(url_for('scopes')) if not room and not center: flash(_('Please, select a room or a center'), category='error') return redirect(url_for('scopes')) if params.get('folder'): flash(_('Error: A folder has been selected. Please, select a room or a center'), category='error') return redirect(url_for('scopes')) form.server.data = params['scope-server'] form.room.data = room form.center.data = center form.submit.render_kw = {"formaction": url_for('action_folder_add_post')} scopes, unused = get_scopes() return render_template('actions/folder_add.html', form=form, parent="scopes.html", scopes=scopes) @app.route('/action/folder/add', methods=['POST']) def action_folder_add_post(): form = FolderForm(request.form) payload = {"name": form.name.data} if form.center.data: payload["center"] = int(form.center.data) if form.room.data: payload["room"] = int(form.room.data) server = get_server_from_ip_port(form.server.data) r = server.post('/folder/add', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('ogServer: error adding folder'), category='error') else: flash(_('Folder added successfully'), category='info') return redirect(url_for("scopes")) @app.route('/action/client/add', methods=['GET', 'POST']) @login_required def action_client_add(): form = ClientDetailsForm(request.form) if request.method == 'POST': if not form.validate(): flash(form.errors, category='error') return redirect(url_for('scopes')) client_name = form.name.data if not client_name: flash(_('Invalid empty client name'), category='error') return redirect(url_for("scopes")) if not is_valid_ip(form.ip.data): flash(_('Invalid IP address'), category='error') return redirect(url_for("scopes")) mac_address = normalize_mac(form.mac.data) if not is_valid_normalized_mac(mac_address): flash(_('Invalid MAC address'), category='error') return redirect(url_for("scopes")) payload = {"boot": form.boot.data, "ip": form.ip.data, "livedir": form.livedir.data, "mac": mac_address, "maintenance": form.maintenance.data, "name": form.name.data, "netdriver": "generic", "netiface": "eth0", "netmask": "0", "remote": form.remote.data, "repo_id": int(form.repo.data), "room": int(form.room.data), "serial_number": form.serial_number.data, "folder_id": int(form.folder_id.data) } server = get_server_from_ip_port(form.server.data) r = server.post('/client/add', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('ogServer: error adding client'), category='error') else: flash(_('Client added successfully'), category='info') return redirect(url_for("scopes")) else: params = request.args.to_dict() if not params.get('scope-room'): flash(_('Please, select a room or a folder'), category='error') return redirect(url_for('scopes')) form.server.data = params['scope-server'] server = get_server_from_ip_port(params['scope-server']) r = server.get('/mode') if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') available_modes = [(mode, mode) for mode in r.json()['modes'] if mode == 'pxe'] available_modes.extend([(mode, mode) for mode in r.json()['modes'] if mode != 'pxe']) form.boot.choices = list(available_modes) form.mac.render_kw = {'placeholder': 'aa:bb:cc:dd:ee:aa'} r = server.get('/scopes') if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') room_id = params['scope-room'] rooms = parse_scopes_from_tree(r.json(), 'room') rooms = [(room['id'], room['name']) for room in rooms if room['id'] == int(room_id)] form.room.choices = list(rooms) form.room.render_kw = {'readonly': True} try: repositories = get_repositories(server) except ServerError: return ogserver_down('scopes') except ServerErrorCode: return ogserver_error('scopes') form.repo.choices = [(repo["id"], repo["name"]) for repo in repositories] if params.get('folder'): form.folder_id.data = params['folder'] else: form.folder_id.data = 0 form.submit.render_kw = {"formaction": url_for('action_client_add')} scopes, clients = get_scopes() return render_template('actions/client_add.html', form=form, parent="scopes.html", scopes=scopes) def build_client_move_choices(scopes, choices, path): for scope in scopes.get('scope'): new_path = f'{path}{scope.get("name")}/' if scope.get('type') == 'room': choices.append((f'{scope["id"]} 0', new_path)) for room_child in scope.get('scope'): if room_child.get('type') == 'folder': choices.append( (f'{scope["id"]} {room_child["id"]}', f'{new_path}{room_child.get("name")}/') ) else: build_client_move_choices(scope, choices, new_path) def get_client_move_choices(scopes): choices = [] build_client_move_choices(scopes, choices, '') return choices @app.route('/action/client/move', methods=['GET', 'POST']) @login_required def action_client_move(): form = ClientMoveForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') if not validate_elements(ips): return redirect(url_for('scopes')) room_id, folder_id = form.scopes.data.split(' ') payload = {"clients": ips, "room": int(room_id), "folder_id": int(folder_id)} server = get_server_from_clients(ips) r = server.post('/client/move', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('ogServer: error moving client'), category='error') else: flash(_('Client moved successfully'), category='info') return redirect(url_for('scopes')) else: ips = parse_elements(request.args.to_dict()) form.ips.data = " ".join(ips) if not validate_elements(ips): return redirect(url_for('scopes')) scopes, clients = get_scopes(set(ips)) form.scopes.choices = get_client_move_choices(scopes) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/client_move.html', form=form, selected_clients=selected_clients, scopes=scopes) PLACEHOLDER_CLIENT_IMPORT_TEXT = '''client_name1,94:c6:91:a6:25:1a,10.141.10.100 client_name2,94:c6:91:a6:25:1b,10.141.10.101''' @app.route('/action/clients/import', methods=['GET']) @login_required def action_clients_import_get(): params = request.args.to_dict() if not params.get('scope-room'): flash(_('Please, select one room'), category='error') return redirect(url_for('scopes')) form = ImportClientsForm() form.server.data = params['scope-server'] server = get_server_from_ip_port(params['scope-server']) r = server.get('/scopes') if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') rooms = parse_scopes_from_tree(r.json(), 'room') selected_room_id = params['scope-room'] selected_room = [(room['id'], room['name'] + " (" + room['parent'] + ")") for room in rooms if room['id'] == int(selected_room_id)] form.room.choices = selected_room form.room.render_kw = {'readonly': True} try: repositories = get_repositories(server) except ServerError: return ogserver_down('scopes') except ServerErrorCode: return ogserver_error('scopes') form.repo.choices = [(repo["id"], repo["name"]) for repo in repositories] form.client_conf.render_kw = {'placeholder': PLACEHOLDER_CLIENT_IMPORT_TEXT} scopes, _clients = get_scopes() return render_template('actions/import_clients.html', form=form, scopes=scopes) OG_CLIENT_DEFAULT_BOOT = "pxe" OG_CLIENT_DEFAULT_LIVEDIR = "ogLive" OG_CLIENT_DEFAULT_MAINTENANCE = False OG_CLIENT_DEFAULT_NETDRIVER = "generic" OG_CLIENT_DEFAULT_NETIFACE = "eth0" OG_CLIENT_DEFAULT_REMOTE = False @app.route('/action/clients/import', methods=['POST']) @login_required def action_clients_import_post(): form = ImportClientsForm(request.form) server = get_server_from_ip_port(form.server.data) clients = [] for client_data in form.client_conf.data.splitlines(): client_values = split_csv(client_data) if len(client_values) != 3 or any(not v for v in client_values): flash(_('Each client requires 3 values'), category='error') return redirect(url_for('scopes')) client_name = client_values[0] client_mac = client_values[1] normalized_mac = normalize_mac(client_mac) if not is_valid_normalized_mac(normalized_mac): flash(_(f'Invalid MAC address {client_mac}'), category='error') return redirect(url_for("scopes")) client_ip = client_values[2] if not is_valid_ip(client_ip): flash(_(f'Invalid IP address {client_ip}'), category='error') return redirect(url_for("scopes")) clients.append({ 'name': client_name, 'mac': normalized_mac, 'ip': client_ip }) if not clients: flash(_('No client configuration found'), category='error') return redirect(url_for('scopes')) payload = {'boot': OG_CLIENT_DEFAULT_BOOT, 'livedir': OG_CLIENT_DEFAULT_LIVEDIR, 'maintenance': OG_CLIENT_DEFAULT_MAINTENANCE, 'netdriver': OG_CLIENT_DEFAULT_NETDRIVER, 'netiface': OG_CLIENT_DEFAULT_NETIFACE, 'netmask': "0", 'remote': OG_CLIENT_DEFAULT_REMOTE, "repo_id": int(form.repo.data), 'room': int(form.room.data)} for client in clients: for key, value in client.items(): payload[key] = value resp = server.post('/client/add', payload) if resp.status_code != requests.codes.ok: flash(_('ogServer: error adding client {}').format(client['name']), category='error') return redirect(url_for('scopes')) flash(_('Clients imported successfully'), category='info') return redirect(url_for('scopes')) def get_selected_clients(scopes): selected_clients = dict() for scope in scopes: scope_type = scope.get('type') selected = scope.get('selected') if ((scope_type == 'computer') and selected): name_id = scope.get('name') + '_' + str(scope.get('id')) selected_clients[name_id] = scope.get('ip')[0] else: selected_clients.update(get_selected_clients(scope['scope'])) return selected_clients @app.route('/action/client/delete', methods=['GET', 'POST']) @login_required def action_client_delete(): form = GenericForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') if not validate_elements(ips): return redirect(url_for('scopes')) payload = {'clients': ips} server = get_server_from_clients(ips) r = server.post('/client/delete', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('ogServer: error deleting client'), category='error') else: flash(_('Client deleted successfully'), category='info') return redirect(url_for('scopes')) else: ips = parse_elements(request.args.to_dict()) form.ips.data = " ".join(ips) if validate_elements(ips): scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/delete_client.html', form=form, selected_clients=selected_clients, scopes=scopes) else: return redirect(url_for('scopes')) @app.route('/action/script/run', methods=['GET', 'POST']) @login_required def action_run_script(): form = RunScriptForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') if not validate_elements(ips): return redirect(url_for('commands')) arguments = form.arguments.data.split(' ') cmd_elems = [form.script.data] + arguments payload = { 'clients': ips, 'run': ';|\n\r'.join(cmd_elems), 'echo': True } server = get_server_from_clients(ips) r = server.post('/shell/run', payload) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') flash(_('Script run sent successfully'), category='info') return redirect(url_for('commands')) else: ips = parse_elements(request.args.to_dict()) form.ips.data = " ".join(ips) if not validate_elements(ips): return redirect(url_for('commands')) server = get_server_from_clients(ips) different_setups = False reference_patitioning = None for ip in ips: r = server.get('/client/setup', payload={'client': [ip]}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') partitions = r.json()['partitions'][1:] if not reference_patitioning: reference_patitioning = partitions elif reference_patitioning != partitions: different_setups = True if different_setups: flash(_('Some clients don\'t have same configuration'), category='info') r = server.get('/shell/list') if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') scripts = r.json()['scripts'] for script in scripts: form.script.choices.append((script, script)) scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/script_run.html', form=form, selected_clients=selected_clients, scopes=scopes) @app.route('/action/script/output', methods=['GET']) @login_required def action_script_display_output(): ips = parse_elements(request.args.to_dict()) if not validate_elements(ips): return redirect(url_for('commands')) server = get_server_from_clients(ips) r = server.get('/shell/output', payload={'clients': list(ips)}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') client_data = r.json()['clients'] for client in client_data: if not 'output' in client: client['output'] = _('No output') if not 'retcode' in client: client['retcode'] = 0 if not 'cmd' in client: client['cmd'] = ['Null'] if not 'tstamp' in client: client['tstamp'] = _('No time available') else: timestamp_utc = datetime.datetime.utcfromtimestamp(client['tstamp']) client['tstamp'] = timestamp_utc.strftime('%Y-%m-%d %H:%M:%S') client_data.sort(key=lambda x:x['tstamp'], reverse=True) scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/script_output.html', selected_clients=selected_clients, scopes=scopes, client_data=client_data) def get_clients_modes(ips, server): modes = {} for ip in ips: r = server.get('/client/info', payload={"client": [ip]}) if not r: raise ServerError if r.status_code != requests.codes.ok: raise ServerErrorCode resp = r.json() current_boot = resp['boot'] client_name = resp['name'] if current_boot not in modes: modes[current_boot] = [client_name] else: modes[current_boot].append(client_name) return modes @app.route('/action/mode', methods=['GET', 'POST']) @login_required def action_mode(): form = BootModeForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') payload = { 'clients': ips, 'mode': form.boot.data } server = get_server_from_clients(ips) r = server.post('/mode', payload) if not r: return ogserver_down('commands') if r.status_code == requests.codes.ok: flash(_('Client set boot mode request sent successfully'), category='info') else: flash(_('Ogserver replied with status code not ok'), category='error') return redirect(url_for('commands')) else: ips = parse_elements(request.args.to_dict()) form.ips.data = " ".join(ips) if not validate_elements(ips): return redirect(url_for('commands')) server = get_server_from_clients(ips) try: modes_set = get_clients_modes(ips, server) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') r = server.get('/mode') if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') mode_descriptions = { 'pxe': 'ogLive (pxe)', '11': 'Disk 1 Partition 1 (11)', '19pxeADMIN': 'ogLive Debug (19pxeADMIN)', '12': 'Disk 1 Partition 2 (12)', '13': 'Disk 1 Partition 3 (13)', 'memtest': 'Memory Test (memtest)', } excluded_modes = ['00unknown', '10'] most_used_mode = max(modes_set, key=lambda m: len(modes_set[m])) available_modes = [] if most_used_mode in r.json()['modes']: most_used_t = (most_used_mode, mode_descriptions.get(most_used_mode, most_used_mode)) available_modes.append(most_used_t) for mode in r.json()['modes']: if mode != most_used_mode and mode not in excluded_modes: available_modes.append((mode, mode_descriptions.get(mode, mode))) if not available_modes: flash(_('no boot templates are available in the server'), category='error') return redirect(url_for('commands')) form.boot.choices = list(available_modes) form.ok.render_kw = { 'formaction': url_for('action_mode') } scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/mode.html', form=form, scopes=scopes, selected_clients=selected_clients, clients=clients, modes_set=modes_set) def get_clients_oglive(ips, server): oglives = {} for ip in ips: r = server.get('/client/info', payload={"client": [ip]}) if not r: raise ServerError if r.status_code != requests.codes.ok: raise ServerErrorCode resp = r.json() oglive = resp['livedir'] if oglive not in oglives: oglives[oglive] = [ip] else: oglives[oglive].append(ip) return oglives @app.route('/action/oglive', methods=['GET', 'POST']) @login_required def action_oglive(): form = OgliveForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') payload = {'clients': ips, 'name': form.oglive.data} server = get_server_from_clients(ips) r = server.post('/oglive/set', payload) if not r: return ogserver_down('commands') if r.status_code == requests.codes.ok: flash(_('Client set ogLive request sent successfully'), category='info') else: flash(_('Ogserver replied with status code not ok'), category='error') return redirect(url_for('commands')) else: ips = parse_elements(request.args.to_dict()) form.ips.data = " ".join(ips) if not validate_elements(ips): return redirect(url_for('commands')) server = get_server_from_clients(list(ips)) try: oglives_set = get_clients_oglive(ips, server) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') r = server.get('/oglive/list') if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') oglive_list = r.json()['oglive'] most_used_live = max(oglives_set, key=lambda l: len(oglives_set[l])) available_oglives = [] for oglive in oglive_list: live_entry = (oglive.get('directory'), oglive.get('directory')) if oglive.get('directory') == most_used_live: available_oglives.insert(0, live_entry) else: available_oglives.append(live_entry) default_idx = int(r.json()['default']) default_oglive_dir = oglive_list[default_idx].get('directory') available_oglives.append(('default', f'ogLive ({default_oglive_dir})')) form.oglive.choices = available_oglives form.ok.render_kw = {'formaction': url_for('action_oglive')} scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/oglive.html', oglives_set=oglives_set, form=form, scopes=scopes, selected_clients=selected_clients) def get_clients_repo_dictionary(ips, server, repositories): repos = {} repo_id_to_name = {repo["id"]: repo["name"] for repo in repositories} for ip in ips: r = server.get('/client/info', payload={"client": [ip]}) if not r: raise ServerError if r.status_code != requests.codes.ok: raise ServerErrorCode resp = r.json() repo_name = repo_id_to_name[resp['repo_id']] if repo_name not in repos: repos[repo_name] = [ip] else: repos[repo_name].append(ip) return repos @app.route('/action/repo/set', methods=['GET', 'POST']) @login_required def action_repo_set(): form = SetRepoForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') payload = {'clients': ips, 'id': int(form.repo.data)} server = get_server_from_clients(ips) r = server.post('/client/repo', payload) if not r: return ogserver_down('commands') if r.status_code == requests.codes.ok: flash(_('Repo set ogLive request sent successfully'), category='info') else: flash(_('Ogserver replied with status code not ok'), category='error') return redirect(url_for('commands')) else: ips = parse_elements(request.args.to_dict()) form.ips.data = " ".join(ips) if not validate_elements(ips): return redirect(url_for('commands')) server = get_server_from_clients(list(ips)) try: repositories = get_repositories(server) repos_set = get_clients_repo_dictionary(ips, server, repositories) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') form.repo.choices = [(repo["id"], repo["name"]) for repo in repositories] scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/repo_set.html', repos_set=repos_set, form=form, scopes=scopes, selected_clients=selected_clients) @app.route('/action/image/create', methods=['GET', 'POST']) @login_required def action_image_create(): form = ImageCreateForm(request.form) if request.method == 'POST': ip = form.ip.data server = get_server_from_clients([ip]) r = server.get('/client/info', payload={"client": [ip]}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') image_name = remove_accents(form.name.data.strip()) if ' ' in image_name: flash(_('No spaces allowed in image names'), category='error') return redirect(url_for('commands')) disk, partition, code = form.os.data.split(' ') payload = {"clients": [ip], "disk": disk, "partition": partition, "code": code, "name": image_name, "repository_id": int(form.repository.data), "id": "0", # This is ignored by the server. "description": form.description.data, "group_id": 0, # Default group. "center_id": r.json()["center"]} r = server.post('/image/create', payload) if not r: return ogserver_down('commands') if r.status_code == requests.codes.ok: return redirect(url_for('commands')) return make_response("400 Bad Request", 400) else: ips = parse_elements(request.args.to_dict()) form.ip.data = " ".join(ips) if not validate_elements(ips, max_len=1): return redirect(url_for('commands')) server = get_server_from_clients(ips) r = server.get('/client/setup', payload={'client': list(ips)}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') invalid_part_types = get_invalid_image_partition_types() for part in r.json()['partitions'][1:]: part_type = PART_TYPE_CODES.get(int(part.get('code')), 'UNKNOWN') if part_type in invalid_part_types: continue if part.get('disk') != 1: continue form.os.choices.append( (f"{part.get('disk')} {part.get('partition')} {part.get('code')}", f"Disk {part.get('disk')} | Partition {part.get('partition')} " f"| {PART_TYPE_CODES.get(part.get('code'), 'UNKNOWN')} " f"{FS_CODES.get(part.get('filesystem'), 'UNKNOWN')}") ) r = server.get('/client/info', payload={'client': list(ips)}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') client_repo_id = r.json()['repo_id'] try: repositories = get_repositories(server) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') form.repository.choices = [ (repo['id'], repo['name']) for repo in repositories if client_repo_id == repo['id']] form.repository.render_kw = {'readonly': True} scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/image_create.html', form=form, scopes=scopes, selected_clients=selected_clients) @app.route('/action/image/update', methods=['GET', 'POST']) @login_required def action_image_update(): form = ImageUpdateForm(request.form) if request.method == 'POST': ip = form.ip.data disk, partition, code = form.os.data.split(' ') image_id = form.image.data server = get_server_from_clients([ip]) r = server.get('/images') if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') images_list = r.json()['images'] image = search_image(images_list, int(image_id)) if not image: flash(_('Image to restore was not found'), category='error') return redirect(url_for('commands')) try: repository = get_repository(image['repo_id'], server) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') payload = {'clients': [ip], 'disk': disk, 'partition': partition, 'code': code, 'name': image['name'], 'repository_id': int(repository['id']), 'id': str(image['id']), 'backup': form.backup.data, # Dummy parameters, not used by ogServer on image update. 'group_id': 0, 'center_id': 0} r = server.post('/image/update', payload) if not r: return ogserver_down('commands') if r.status_code == requests.codes.ok: flash(_('Image update command sent sucessfully'), category='info') else: flash(_('There was a problem sending the image update command'), category='error') return redirect(url_for('commands')) params = request.args.to_dict() center_id = int(params.get('scope-center')) ips = parse_elements(params) if not validate_elements(ips, max_len=1): return redirect(url_for('commands')) form.ip.data = ' '.join(ips) server = get_server_from_clients(ips) r = server.get('/client/info', payload={'client': list(ips)}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') repo_id = r.json()['repo_id'] try: images = get_images_from_repo(server, repo_id) if not images: flash(_(f'Computer(s) assigned to a repo with no images'), category='error') return redirect(url_for('commands')) images = filter_images_allowed_in_center(server, images, center_id) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') sort_images(images) for image in images: form.image.choices.append((image['id'], image['name'])) r = server.get('/client/setup', payload={'client': list(ips)}) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') invalid_part_types = get_invalid_image_partition_types() part_content = {} for part in r.json()['partitions'][1:]: part_type = PART_TYPE_CODES.get(int(part.get('code')), 'UNKNOWN') if part_type in invalid_part_types: continue if part.get('disk') != 1: continue partition_value = f"{part.get('disk')} {part.get('partition')} {part.get('code')}" partition_text = f"Disk {part.get('disk')} | Partition {part.get('partition')} " f"| {PART_TYPE_CODES.get(part.get('code'), 'UNKNOWN')} " f"{FS_CODES.get(part.get('filesystem'), 'UNKNOWN')}" form.os.choices.append( (partition_value, f"Disk {part.get('disk')} | Partition {part.get('partition')} " f"| {PART_TYPE_CODES.get(part.get('code'), 'UNKNOWN')} " f"{FS_CODES.get(part.get('filesystem'), 'UNKNOWN')}") ) if part['image']: for image in images: if image['id'] == part['image']: part_content[partition_value] = part['image'] break scopes, _clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/image_update.html', form=form, selected_clients=selected_clients, scopes=scopes, part_content=part_content) @app.route('/action/reboot', methods=['GET', 'POST']) @login_required def action_reboot(): form = GenericForm(request.form) if request.method == 'POST': ips = form.ips.data.split(' ') if not validate_elements(ips): return redirect(url_for('commands')) payload = {'clients': ips} server = get_server_from_clients(ips) r = server.post('/reboot', payload) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: flash(_('ogServer: error rebooting client'), category='error') else: flash(_('Client rebooted successfully'), category='info') return redirect(url_for('commands')) else: ips = parse_elements(request.args.to_dict()) form.ips.data = " ".join(ips) if validate_elements(ips): scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/reboot.html', form=form, selected_clients=selected_clients, scopes=scopes) else: return redirect(url_for('commands')) @app.route('/action/refresh', methods=['POST']) @login_required def action_refresh(): ips = parse_elements(request.form.to_dict()) if not validate_elements(ips): return redirect(url_for('commands')) server = get_server_from_clients(list(ips)) payload = {'clients': list(ips)} r = server.post('/refresh', payload) if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: flash(_('OgServer replied with a non ok status code'), category='error') else: flash(_('Refresh request processed successfully'), category='info') return redirect(url_for('commands')) @app.route('/action/center/add', methods=['GET', 'POST']) @login_required def action_center_add(): form = CenterForm(request.form) if request.method == 'POST': payload = {"name": form.name.data, "comment": form.comment.data} server = get_server_from_ip_port(form.server.data) r = server.post('/center/add', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('Server replied with error code when adding the center'), category='error') else: flash(_('Center added successfully'), category='info') return redirect(url_for("scopes")) else: server_choices = [(server.ip + ':' + str(server.port), server.name) for server in servers] form.server.choices = server_choices scopes, clients = get_scopes() return render_template('actions/add_center.html', form=form, scopes=scopes) @app.route('/action/center/update', methods=['GET', 'POST']) @login_required def action_center_update(): form = CenterForm(request.form) if request.method == 'POST': payload = {"id": int(form.center.data), "name": form.name.data, "comment": form.comment.data} server = get_server_from_ip_port(form.server.data) r = server.post('/center/update', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('Server replied with error code when updating the center'), category='error') else: flash(_('Center updated successfully'), category='info') return redirect(url_for("scopes")) else: params = request.args.to_dict() server = params.get('scope-server') center = params.get('scope-center') if not center: flash(_('Please, select one center'), category='error') return redirect(url_for("scopes")) if not server: flash(_('Internal error: server was not sent as request argument'), category='error') return redirect(url_for("scopes")) server = get_server_from_ip_port(server) form.server.choices = [(server.ip + ':' + str(server.port), server.name)] form.server.render_kw = {'readonly': True} form.center.data = center payload = {"id": int(center)} r = server.get('/center/info', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') form.comment.data = r.json()['comment'] form.name.data = r.json()['name'] scopes, clients = get_scopes() form.submit.render_kw = {"formaction": url_for('action_center_update')} return render_template('actions/center_update.html', form=form, scopes=scopes) @app.route('/action/center/info', methods=['GET']) @login_required def action_center_info(): form = CenterForm(request.form) params = request.args.to_dict() server = params.get('scope-server') center = params.get('scope-center') if not center: flash(_('Please, select one center'), category='error') return redirect(url_for("scopes")) if not server: flash(_('Internal error: server was not sent as request argument'), category='error') return redirect(url_for("scopes")) server = get_server_from_ip_port(server) form.server.choices = [(server.ip + ':' + str(server.port), server.name)] form.server.render_kw = {'readonly': True} form.center.data = center form.center.render_kw = {'readonly': True} payload = {"id": int(center)} r = server.get('/center/info', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') form.comment.data = r.json()['comment'] form.comment.render_kw = {'readonly': True} form.name.data = r.json()['name'] form.name.render_kw = {'readonly': True} form.submit.render_kw = {'readonly': True, 'hidden': True} scopes, clients = get_scopes() return render_template('actions/center_details.html', form=form, scopes=scopes) def get_scope_context_rec(elem_id, elem_type, scopes, ancestors): if not scopes: return ([], None) res = None for s in scopes: if s['type'] == elem_type and int(s['id']) == elem_id: ancestors.append(s['name']) return (ancestors, s) ancestors_tmp = list(ancestors) ancestors_tmp.append(s['name']) ancestors_tmp, elem = get_scope_context_rec(elem_id, elem_type, s['scope'], ancestors_tmp) if elem: res = (ancestors_tmp, elem) break if res: return res else: return ([], None) def find_element_scope(elem_id, elem_type, scopes): unused, elem = get_scope_context_rec(elem_id, elem_type, scopes['scope'], []) return elem def get_scope_context(elem_id, elem_type, scopes): ancestors, elem = get_scope_context_rec(elem_id, elem_type, scopes['scope'], []) children = {} for c in elem['scope']: if c['type'] not in children: children[c['type']] = [] children[c['type']].append(c['name']) return (ancestors, children) @app.route('/action/center/delete', methods=['GET', 'POST']) @login_required def action_center_delete(): form = DeleteCenterForm(request.form) if request.method == 'POST': server = get_server_from_ip_port(form.server.data) payload = {"id": form.center.data} r = server.post('/center/delete', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('Server replied with error code when deleting the center'), category='error') else: flash(_('Center deleted successfully'), category='info') return redirect(url_for("scopes")) else: params = request.args.to_dict() if not params.get('scope-center'): flash(_('Please, select one center'), category='error') return redirect(url_for('scopes')) server = get_server_from_ip_port(params['scope-server']) r = server.get('/scopes') if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') form.center.data = params['scope-center'] form.server.data = params['scope-server'] scopes, clients = get_scopes() ancestors, children = get_scope_context(int(params['scope-center']), 'center', scopes) return render_template('actions/delete_center.html', form=form, scopes=scopes, ancestors=ancestors, children=children) @app.route('/action/room/add', methods=['GET', 'POST']) @login_required def action_room_add(): form = RoomForm(request.form) if request.method == 'POST': server = get_server_from_ip_port(form.server.data) payload = {"center": int(form.center.data), "name": form.name.data, "netmask": form.netmask.data, "gateway": form.gateway.data, "folder_id": int(form.folder_id.data)} r = server.post('/room/add', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('Server replied with error code when adding the room'), category='error') else: flash(_('Room added successfully'), category='info') return redirect(url_for("scopes")) else: params = request.args.to_dict() if not params.get('scope-center'): flash(_('Please, select a center or a folder'), category='error') return redirect(url_for('scopes')) server = get_server_from_ip_port(params['scope-server']) r = server.get('/scopes') if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') selected_center_id = params['scope-center'] centers = parse_scopes_from_tree(r.json(), 'center') selected_center = [(center['id'], center['name']) for center in centers if center['id'] == int(selected_center_id)] form.center.choices = selected_center form.center.render_kw = {'readonly': True} form.server.data = params['scope-server'] if params.get('folder'): form.folder_id.data = params['folder'] else: form.folder_id.data = 0 scopes, clients = get_scopes() return render_template('actions/add_room.html', form=form, scopes=scopes) @app.route('/action/room/update', methods=['GET', 'POST']) @login_required def action_room_update(): form = RoomForm(request.form) if request.method == 'POST': server = get_server_from_ip_port(form.server.data) payload = {"name": form.name.data, "netmask": form.netmask.data, "gateway": form.gateway.data, "id": int(form.room.data)} r = server.post('/room/update', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('Server replied with error code when updating the room'), category='error') else: flash(_('Room updated successfully'), category='info') return redirect(url_for("scopes")) else: params = request.args.to_dict() room_id = params.get('scope-room') if not room_id: flash(_('Please, select a room to update'), category='error') return redirect(url_for('scopes')) server = get_server_from_ip_port(params['scope-server']) del form.center form.server.data = params['scope-server'] form.room.data = room_id payload = {"id": int(room_id)} r = server.get('/room/info', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') form.name.data = r.json()['name'] form.gateway.data = r.json()['gateway'] form.netmask.data = r.json()['netmask'] form.submit.render_kw = {"formaction": url_for('action_room_update')} scopes, clients = get_scopes() return render_template('actions/room_update.html', form=form, scopes=scopes) @app.route('/action/room/info', methods=['GET']) @login_required def action_room_info(): form = RoomForm(request.form) params = request.args.to_dict() room_id = params.get('scope-room') if not room_id: flash(_('Please, select a room to update'), category='error') return redirect(url_for('scopes')) server = get_server_from_ip_port(params['scope-server']) del form.center payload = {"id": int(room_id)} r = server.get('/room/info', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') form.name.data = r.json()['name'] form.name.render_kw = {'readonly': True} form.gateway.data = r.json()['gateway'] form.gateway.render_kw = {'readonly': True} form.netmask.data = r.json()['netmask'] form.netmask.render_kw = {'readonly': True} form.submit.render_kw = {'readonly': True, 'hidden': True} scopes, clients = get_scopes() return render_template('actions/room_details.html', form=form, scopes=scopes) @app.route('/action/room/delete', methods=['GET', 'POST']) @login_required def action_room_delete(): form = DeleteRoomForm(request.form) if request.method == 'POST': payload = {"id": form.room.data} server = get_server_from_ip_port(form.server.data) r = server.post('/room/delete', payload) if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: flash(_('Server replied with error code when deleting the room'), category='error') else: flash(_('Room deleted successfully'), category='info') return redirect(url_for("scopes")) else: params = request.args.to_dict() if not params.get('scope-room'): flash(_('Please, select one room'), category='error') return redirect(url_for('scopes')) server = get_server_from_ip_port(params['scope-server']) r = server.get('/scopes') if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') form.room.data = params['scope-room'] form.room.render_kw = {'readonly': True} form.server.data = params['scope-server'] scopes, clients = get_scopes() ancestors, children = get_scope_context(int(params['scope-room']), 'room', scopes) return render_template('actions/delete_room.html', form=form, scopes=scopes, ancestors=ancestors, children=children) @app.route('/commands/', methods=['GET']) @login_required def commands(): scopes, clients = get_scopes() return render_template('commands.html', scopes=scopes, clients=clients) def _get_sorted_repos(repos): return dict(sorted(repos.items(), key=lambda item: item[1]['name'])) def get_images_grouped_by_repos(): responses = multi_request('get', '/images') servers=[] for resp in responses: server={} server['server'] = resp['server'] images=resp['json']['images'] try: all_repos=get_repositories(resp['server']) except ServerError: continue except ServerErrorCode: continue repos={} sort_images(images) for image in images: repo_id=image['repo_id'] repo_data={} if repo_id not in repos: image_repo = [repo['name'] for repo in all_repos if repo_id == repo['id']] repos[repo_id] = {} if image_repo: repos[repo_id]['name'] = image_repo[0] else: repos[repo_id]['name'] = 'unknown' repos[repo_id]['images'] = [image] else: repos[repo_id]['images'].append(image) server['repos'] = _get_sorted_repos(repos) servers.append(server) return servers @app.route('/images/', methods=['GET']) @login_required def images(): responses = get_images_grouped_by_repos() return render_template('images.html', responses=responses) @app.route('/repos/', methods=['GET']) @login_required def manage_repos(): responses = get_all_repositories() return render_template('repos.html', repos_resp=responses) def repo_addr_is_valid(form): invalid_ips = [] empty_ips = 0 ip_count = 0 for ip in form.addr: ip_count += 1 ip = ip.data.strip() if not ip: empty_ips += 1 continue if not is_valid_ip(ip): invalid_ips.append('"' + ip + '"') res = True if empty_ips > 0: res = False flash(_(f'{empty_ips} addresses are invalid'), category='error') if invalid_ips: res = False flash(_(f'The following addresses are invalid: {" ".join(invalid_ips)}'), category='error') MAX_IP_COUNT = 128 if ip_count > MAX_IP_COUNT: res = False flash(_(f'More than {MAX_IP_COUNT} addresses is not supported'), category='error') return res @app.route('/action/repo/add', methods=['POST', 'GET']) @login_required def action_repo_add(): form = RepoForm(request.form) if request.method == 'POST': if not form.validate(): flash(form.errors, category='error') return redirect(url_for('manage_repos')) if not repo_addr_is_valid(form): return redirect(url_for('manage_repos')) addr = [ip.data.strip() for ip in form.addr] payload = {"name": form.name.data, "addr": addr, "center": 1} server = get_server_from_ip_port(form.server.data) r = server.post('/repository/add', payload) if not r: return ogserver_down('manage_repos') if r.status_code != requests.codes.ok: flash(_('ogServer: error adding repo'), category='error') else: flash(_('Repo added successfully'), category='info') return redirect(url_for("manage_repos")) else: params = request.args.to_dict() if not params.get('repos-server'): flash(_('Please, select a server'), category='error') return redirect(url_for('manage_repos')) form.server.data = params['repos-server'] form.addr.append_entry('') responses = get_all_repositories() return render_template('actions/repos_add.html', form=form, repos_resp=responses) @app.route('/action/repo/update', methods=['GET', 'POST']) @login_required def action_repo_update(): form = RepoForm(request.form) if request.method == 'POST': server = get_server_from_ip_port(form.server.data) if not repo_addr_is_valid(form): return redirect(url_for('manage_repos')) addr = [ip.data.strip() for ip in form.addr] payload = { 'id': int(form.repo_id.data), 'name': form.name.data, 'addr': addr, 'center': 1} r = server.post('/repository/update', payload) if not r: return ogserver_down('manage_repos') if r.status_code != requests.codes.ok: flash(_('ogServer: error updating repo'), category='error') else: flash(_('Repo updated successfully'), category='info') return redirect(url_for('manage_repos')) else: params = request.args.to_dict() repos = parse_elements(params) if not validate_elements(repos, max_len=1): return redirect(url_for('manage_repos')) repo_id = repos.pop() server_ip_port = params.get('repos-server') if not server_ip_port: flash(_('Please, select a server'), category='error') return redirect(url_for('manage_repos')) server = get_server_from_ip_port(server_ip_port) try: repository = get_repository(int(repo_id), server) except ServerError: return ogserver_down('manage_repos') except ServerErrorCode: return ogserver_error('manage_repos') form.server.data = server_ip_port form.repo_id.data = repo_id form.name.data = repository['name'] for addr in repository['addr']: form.addr.append_entry(addr) responses = get_all_repositories() return render_template('actions/repos_update.html', form=form, repos_resp=responses) @app.route('/action/repo/delete', methods=['GET', 'POST']) @login_required def action_repo_delete(): form = RepoForm(request.form) if request.method == 'POST': server = get_server_from_ip_port(form.server.data) payload = { 'id': int(form.repo_id.data) } r = server.post('/repository/delete', payload) if not r: return ogserver_down('manage_repos') if r.status_code != requests.codes.ok: flash(_('ogServer: error deleting repo'), category='error') else: flash(_('Repo deleted successfully'), category='info') return redirect(url_for('manage_repos')) else: params = request.args.to_dict() repos = parse_elements(params) if not validate_elements(repos, max_len=1): return redirect(url_for('manage_repos')) repo_id = repos.pop() if not repo_id: flash(_('Please, select a repo'), category='error') return redirect(url_for('manage_repos')) repo_id = int(repo_id) server_ip_port = params.get('repos-server') if not server_ip_port: flash(_('Please, select a server'), category='error') return redirect(url_for('manage_repos')) server = get_server_from_ip_port(server_ip_port) try: repository = get_repository(repo_id, server) except ServerError: return ogserver_down('manage_repos') except ServerErrorCode: return ogserver_error('manage_repos') form.server.data = server_ip_port form.repo_id.data = repo_id form.name.data = repository['name'] form.name.render_kw = {'readonly': True} form.name.data = repository['name'] for addr in repository['addr']: form.addr.append_entry(addr) for field in form.addr: field.render_kw = {'readonly': True} responses = get_all_repositories() return render_template('actions/delete_repo.html', form=form, repos_resp=responses) @app.route('/action/repo/info', methods=['GET']) @login_required def action_repo_info(): form = RepoForm() params = request.args.to_dict() repos = parse_elements(params) if not validate_elements(repos, max_len=1): return redirect(url_for('manage_repos')) repo_id = repos.pop() if not repo_id: flash(_('Please, select a repo'), category='error') return redirect(url_for('manage_repos')) repo_id = int(repo_id) server_ip_port = params.get('repos-server') if not server_ip_port: flash(_('Please, select a server'), category='error') return redirect(url_for('manage_repos')) server = get_server_from_ip_port(server_ip_port) try: repository = get_repository(repo_id, server) except ServerError: return ogserver_down('manage_repos') except ServerErrorCode: return ogserver_error('manage_repos') form.name.data = repository['name'] form.name.render_kw = {'readonly': True} for addr in repository['addr']: form.addr.append_entry(addr) for field in form.addr: field.render_kw = {'readonly': True} form.submit.render_kw = {"style": "visibility:hidden;"} responses = get_all_repositories() return render_template('actions/repo_details.html', form=form, repos_resp=responses) @app.route('/servers/', methods=['GET']) @login_required def manage_servers(): return render_template('servers.html', servers=servers) @app.route('/server/update', methods=['GET']) @login_required def server_update_get(): params = request.args.to_dict() try: selected_server = get_server_from_ip_port(params['selected-server']) except KeyError: flash(_('Please, select one server'), category='error') return redirect(url_for('manage_servers')) r = selected_server.get('/server') if not r: return ogserver_down('manage_servers') if r.status_code != requests.codes.ok: return ogserver_error('manage_servers') form = ServerConfigurationForm() server_config = r.json()['servers'] for c in server_config: form.addr.append_entry(c['address']) form.server_addr.data = selected_server.ip + ":" + str(selected_server.port) return render_template('actions/server_update.html', form=form, servers=servers) @app.route('/server/update', methods=['POST']) @login_required def server_update_post(): form = ServerConfigurationForm(request.form) try: server = get_server_from_ip_port(form.server_addr.data) except Exception: flash(_('Server {} does not exist').format(form.server_addr.data), category='error') return redirect(url_for('manage_servers')) addr_list = [ip.data.strip() for ip in form.addr] invalid_ips = [] for ip in addr_list: if not is_valid_ip(ip): invalid_ips.append('"' + ip + '"') if invalid_ips: flash(_(f'The following addresses are invalid: {" ".join(invalid_ips)}'), category='error') return redirect(url_for('manage_servers')) r = server.get('/server') if not r: return ogserver_down('manage_servers') if r.status_code != requests.codes.ok: return ogserver_error('manage_servers') server_config = r.json()['servers'] # Remove for c in server_config: if c['address'] in addr_list: continue payload = {'id': c['id']} rd = server.delete('/server', payload=payload) if not rd: return ogserver_down('manage_servers') if rd.status_code != requests.codes.ok: return ogserver_error('manage_servers') # Add for ip in addr_list: found = False for c in server_config: if ip == c['address']: found = True break if found: continue payload = {'address': ip} ra = server.post('/server', payload=payload) if not ra: return ogserver_down('manage_servers') if ra.status_code != requests.codes.ok: return ogserver_error('manage_servers') flash(_('Server update request sent successfully'), category='info') return redirect(url_for('manage_servers')) @app.route('/server/add', methods=['GET']) @login_required def server_add_get(): form = ServerForm() return render_template('actions/add_server.html', form=form, servers=servers) @app.route('/server/add', methods=['POST']) @login_required def server_add_post(): form = ServerForm(request.form) if not form.validate(): flash(form.errors, category='error') return redirect(url_for('manage_servers')) ip_port_str = form.ip.data + ":" + form.port.data try: get_server_from_ip_port(ip_port_str) flash(_('Server {} already exists').format(ip_port_str), category='error') return redirect(url_for('manage_servers')) except Exception: return save_server(form) @app.route('/server/delete', methods=['GET']) @login_required def server_delete_get(): params = request.args.to_dict() try: selected_server = get_server_from_ip_port(params['selected-server']) except KeyError: flash(_('Please, select one server'), category='error') return redirect(url_for('manage_servers')) form = ServerForm() form.name.data = selected_server.name form.name.render_kw = {'readonly': True} form.ip.data = selected_server.ip form.ip.render_kw = {'readonly': True} form.port.data = selected_server.port form.port.render_kw = {'readonly': True} form.api_token.data = selected_server.api_token form.api_token.render_kw = {'readonly': True} return render_template('actions/delete_server.html', form=form, servers=servers) @app.route('/server/delete', methods=['POST']) @login_required def server_delete_post(): form = ServerForm(request.form) if not form.validate(): flash(form.errors, category='error') return redirect(url_for('manage_servers')) ip_port_str = form.ip.data + ":" + form.port.data try: server = get_server_from_ip_port(ip_port_str) return delete_server(server) except Exception: flash(_('Server {} does not exist').format(ip_port_str), category='error') return redirect(url_for('manage_servers')) @app.route('/users/', methods=['GET']) @login_required def users(): users = app.config['USERS'] return render_template('users.html', users=users) def get_available_centers(): responses = multi_request('get', '/scopes') available_centers = list() for resp in responses: centers = parse_scopes_from_tree(resp['json'], 'center') centers = [(center['id'], center['name']) for center in centers] available_centers.extend(centers) return available_centers def get_center_choices(): responses = multi_request('get', '/scopes') available_scopes = list() for resp in responses: centers = parse_scopes_from_tree(resp['json'], 'center') centers = [(str(center['id']), center['name']) for center in centers] available_scopes.extend(centers) return available_scopes def save_server(form): server_dict = { 'NAME': form.name.data, 'IP': form.ip.data, 'PORT': int(form.port.data), 'API_TOKEN': form.api_token.data, } server_obj = OGServer(form.name.data, form.ip.data, int(form.port.data), form.api_token.data) filename = os.path.join(app.root_path, ogcp_cfg_path) with open(filename, 'r+') as file: config = json.load(file) try: config['SERVERS'].append(server_dict) except KeyError: config['SERVERS'] = list() config['SERVERS'].append(server_dict) file.seek(0) json.dump(config, file, indent='\t') file.truncate() servers.append(server_obj) return redirect(url_for('manage_servers')) def delete_server(server): server_dict = { 'NAME': server.name, 'IP': server.ip, 'PORT': int(server.port), 'API_TOKEN': server.api_token, } filename = os.path.join(app.root_path, ogcp_cfg_path) with open(filename, 'r+') as file: config = json.load(file) try: config['SERVERS'].remove(server_dict) except (KeyError, ValueError): config.pop('IP') config.pop('PORT') config.pop('API_TOKEN') file.seek(0) json.dump(config, file, indent='\t') file.truncate() servers.remove(server) return redirect(url_for('manage_servers')) def save_user(form, preserve_pwd): username = form.username.data if preserve_pwd: pwd_hash = form.pwd.data else: pwd_hash = hash_password(form.pwd.data) pwd_hash_confirm = hash_password(form.pwd_confirm.data) if not pwd_hash == pwd_hash_confirm: flash(_('Passwords do not match'), category='error') return False admin = form.admin.data scopes = form.scopes.data user = { 'USER': username, 'PASS': pwd_hash, 'ADMIN': admin, 'SCOPES': scopes, 'PERMISSIONS': { 'CLIENT': { 'ADD': form.client_permissions.add.data, 'UPDATE': form.client_permissions.update.data, 'DELETE': form.client_permissions.delete.data, }, 'CENTER': { 'ADD': form.center_permissions.add.data, 'UPDATE': form.center_permissions.update.data, 'DELETE': form.center_permissions.delete.data, }, 'ROOM': { 'ADD': form.room_permissions.add.data, 'UPDATE': form.room_permissions.update.data, 'DELETE': form.room_permissions.delete.data, }, 'FOLDER': { 'ADD': form.folder_permissions.add.data, 'UPDATE': form.folder_permissions.update.data, 'DELETE': form.folder_permissions.delete.data, }, 'IMAGE': { 'ADD': form.image_permissions.add.data, 'UPDATE': form.image_permissions.update.data, 'DELETE': form.image_permissions.delete.data, }, 'REPOSITORY': { 'ADD': form.repository_permissions.add.data, 'UPDATE': form.repository_permissions.update.data, 'DELETE': form.repository_permissions.delete.data, }, }, } filename = os.path.join(app.root_path, ogcp_cfg_path) with open(filename, 'r+') as file: config = json.load(file) old_user = get_user(username) if old_user: config['USERS'].remove(old_user) config['USERS'].append(user) file.seek(0) json.dump(config, file, indent='\t') file.truncate() if old_user: app.config['USERS'].remove(old_user) app.config['USERS'].append(user) return True def delete_user(username): user = get_user(username) filename = os.path.join(app.root_path, ogcp_cfg_path) with open(filename, 'r+') as file: config = json.load(file) config['USERS'].remove(user) file.seek(0) json.dump(config, file, indent='\t') file.truncate() app.config['USERS'].remove(user) return redirect(url_for('users')) @app.route('/user/add', methods=['GET']) @login_required def user_add_get(): form = UserForm() form.scopes.choices = get_available_centers() return render_template('auth/add_user.html', form=form) @app.route('/user/add', methods=['POST']) @login_required def user_add_post(): form = UserForm(request.form) form.scopes.choices = get_center_choices() if not form.validate(): flash(form.errors, category='error') return redirect(url_for('users')) if get_user(form.username.data): flash(_('This username already exists'), category='error') return redirect(url_for('users')) if save_user(form, preserve_pwd=False): flash(_('User created successfully'), category='info') return redirect(url_for('users')) @app.route('/user/edit', methods=['GET']) @login_required def user_edit_get(): username_set = parse_elements(request.args.to_dict()) if not validate_elements(username_set, max_len=1): return redirect(url_for('users')) username = username_set.pop() user = get_user(username) if not user: flash(_('User {} does not exist').format(username), category='error') return redirect(url_for('users')) form = EditUserForm() form.username.data = user.get('USER') form.username.render_kw = {'readonly': True} form.admin.data = user.get('ADMIN') form.scopes.data = user.get('SCOPES') if 'PERMISSIONS' in user: permissions = user.get('PERMISSIONS') def get_permission(target, action): if not target in permissions: return True return permissions[target].get(action, True) form.client_permissions.add.data = get_permission('CLIENT', 'ADD') form.client_permissions.update.data = get_permission('CLIENT', 'UPDATE') form.client_permissions.delete.data = get_permission('CLIENT', 'DELETE') form.center_permissions.add.data = get_permission('CENTER', 'ADD') form.center_permissions.update.data = get_permission('CENTER', 'UPDATE') form.center_permissions.delete.data = get_permission('CENTER', 'DELETE') form.room_permissions.add.data = get_permission('ROOM', 'ADD') form.room_permissions.update.data = get_permission('ROOM', 'UPDATE') form.room_permissions.delete.data = get_permission('ROOM', 'DELETE') form.folder_permissions.add.data = get_permission('FOLDER', 'ADD') form.folder_permissions.update.data = get_permission('FOLDER', 'UPDATE') form.folder_permissions.delete.data = get_permission('FOLDER', 'DELETE') form.image_permissions.add.data = get_permission('IMAGE', 'ADD') form.image_permissions.update.data = get_permission('IMAGE', 'UPDATE') form.image_permissions.delete.data = get_permission('IMAGE', 'DELETE') form.repository_permissions.add.data = get_permission('REPOSITORY', 'ADD') form.repository_permissions.update.data = get_permission('REPOSITORY', 'UPDATE') form.repository_permissions.delete.data = get_permission('REPOSITORY', 'DELETE') form.scopes.choices = get_available_centers() return render_template('auth/edit_user.html', form=form) @app.route('/user/edit', methods=['POST']) @login_required def user_edit_post(): form = EditUserForm(request.form) form.scopes.choices = get_center_choices() if not form.validate(): flash(form.errors, category='error') return redirect(url_for('users')) username = form.username.data old_user_data = get_user(username) if not old_user_data: flash(_('User {} does not exist').format(username), category='error') return redirect(url_for('users')) preserve_pwd = (not form.pwd.data and not form.pwd_confirm.data) if preserve_pwd: form.pwd.data = old_user_data.get("PASS") if save_user(form, preserve_pwd): flash(_('User edited successfully'), category='info') return redirect(url_for('users')) @app.route('/user/delete', methods=['GET']) @login_required def user_delete_get(): username_set = parse_elements(request.args.to_dict()) if not validate_elements(username_set, max_len=1): return redirect(url_for('users')) username = username_set.pop() user = get_user(username) if not user: flash(_('User {} does not exist').format(username), category='error') return redirect(url_for('users')) form = DeleteUserForm() form.username.data = user.get('USER') return render_template('auth/delete_user.html', form=form) @app.route('/action/image/list', methods=['GET']) @login_required def action_image_list(): params = request.args.to_dict() ids = parse_elements(params) ids = [int(id) for id in ids if id.isdigit()] if not ids: flash(_('Please, select one more images to be listed'), category='error') return redirect(url_for('images')) server = get_server_from_ip_port(params['image-server']) try: responses = get_images_grouped_by_repos() servers = [] for server in responses: repos = [] for unused, repo in server['repos'].items(): images=[] for img in repo['images']: if int(img['id']) in ids: images.append(img) if images: repos.append((repo['name'], images)) if repos: s={} s['name'] = server['server'].name s['repos'] = repos servers.append(s) except ServerError: return ogserver_down('images') except ServerErrorCode: return ogserver_error('images') return render_template('actions/list_images.html', servers=servers, responses=responses) @app.route('/user/delete', methods=['POST']) @login_required def user_delete_post(): form = DeleteUserForm(request.form) if not form.validate(): flash(form.errors, category='error') return redirect(url_for('users')) username = form.username.data if not get_user(username): flash(_('User {} does not exist').format(username), category='error') return redirect(url_for('users')) delete_user(username) flash(_('User {} deleted').format(username), category='info') return redirect(url_for('users')) @app.route('/action/image/info', methods=['GET']) @login_required def action_image_info(): form = ImageDetailsForm() params = request.args.to_dict() ids = parse_elements(params) if not validate_elements(ids, max_len=1): return redirect(url_for('images')) id = ids.pop() server = get_server_from_ip_port(params['image-server']) r = server.get('/images') if not r: return ogserver_down('images') if r.status_code != requests.codes.ok: return ogserver_error('images') images = r.json()['images'] image = next(img for img in images if img['id'] == int(id)) form.name.data = image['name'] # Bytes to Mebibytes form.size.data = image['size'] / 1024 ** 2 form.datasize.data = image['datasize'] / 1024 ** 2 form.modified.data = image['modified'] form.permissions.data = image['permissions'] form.software_id.data = image['software_id'] form.description.data = image['description'] checksum = image.get('checksum', _('Unknown')) if not checksum: checksum = _('Unknown') form.checksum.data = checksum try: responses = get_images_grouped_by_repos() except ServerError: return ogserver_down('images') except ServerErrorCode: return ogserver_error('images') r = server.get('/image/restrict', {'image': image['id']}) if not r: raise ServerError if r.status_code != requests.codes.ok: raise ServerErrorCode form.scopes.choices = get_available_centers() form.scopes.data = [str(scope) for scope in r.json().get('scopes')] return render_template('actions/image_details.html', form=form, responses=responses) @app.route('/action/image/delete', methods=['GET', 'POST']) @login_required def action_image_delete(): form = GenericForm(request.form) if request.method == 'POST': ids = form.ids.data.split(' ') if not validate_elements(ids): return redirect(url_for('images')) server = get_server_from_ip_port(form.server.data) for id in ids: payload = {'image': id} r = server.post('/image/delete', payload) if not r: return ogserver_down('images') if r.status_code != requests.codes.ok: return ogserver_error('images') flash(_('Image deletion request sent successfully'), category='info') return redirect(url_for('images')) else: params = request.args.to_dict() image_ids = [imgid for name, imgid in params.items() if name != 'csrf_token' and name != 'image-server'] if not validate_elements(image_ids): return redirect(url_for('images')) server = get_server_from_ip_port(params['image-server']) try: responses = get_images_grouped_by_repos() except ServerError: return ogserver_down('images') except ServerErrorCode: return ogserver_error('images') form.ids.data = ' '.join(image_ids) form.server.data = params['image-server'] return render_template('actions/delete_image.html', form=form, image_ids=image_ids, responses=responses) @app.route('/action/image/config', methods=['GET', 'POST']) @login_required def action_image_config(): form = ImageConfigForm(request.form) if request.method == 'POST': image_id = int(form.image_id.data) server = get_server_from_ip_port(form.server.data) scope_list = [int(scope) for scope in form.scopes.data] payload = {'image': image_id, 'scopes': scope_list} r = server.post('/image/restrict', payload) if not r: return ogserver_down('images') if r.status_code != requests.codes.ok: return ogserver_error('images') flash(_('Image updated successfully'), category='info') return redirect(url_for('images')) else: params = request.args.to_dict() images = [(name, imgid) for name, imgid in params.items() if name != 'csrf_token' and name != 'image-server'] if not validate_elements(images, max_len=1): return redirect(url_for('images')) image_name, image_id = images[0] image_name=image_name.split('_', 1)[0] server = get_server_from_ip_port(params['image-server']) form.image_id.data = image_id form.name.data = image_name r = server.get('/image/restrict', {'image': int(image_id)}) if not r: return ogserver_down('images') if r.status_code != requests.codes.ok: return ogserver_error('images') form.server.data = params['image-server'] form.scopes.choices = get_available_centers() form.scopes.data = [str(scope) for scope in r.json().get('scopes')] try: responses = get_images_grouped_by_repos() except ServerError: return ogserver_down('images') except ServerErrorCode: return ogserver_error('images') return render_template('actions/image_config.html', form=form, responses=responses) @app.route('/action/log', methods=['GET']) @login_required def action_legacy_log(): ips = parse_elements(request.args.to_dict()) if not validate_elements(ips, max_len=1): return redirect(url_for('commands')) ip = ips.pop() log_file = Path("/opt/opengnsys/log/clients/" + str(ip) + ".log") if not os.access(log_file, os.R_OK): flash(_('No log available for this client yet'), category='error') return redirect(url_for('commands')) log = log_file.read_text() if log: scopes, clients = get_scopes(set(ips)) return render_template('actions/legacy/log.html', log=log, scopes=scopes) else: return redirect(url_for('commands')) @app.route('/action/rt-log', methods=['GET']) @login_required def action_legacy_rt_log(): ips = parse_elements(request.args.to_dict()) if not validate_elements(ips, max_len=1): return redirect(url_for('commands')) ip = ips.pop() scheme = "http://" rt_log_path = "/cgi-bin/httpd-log.sh" rt_log_url = scheme + ip + rt_log_path return redirect(rt_log_url)