# 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 ) 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 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 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', 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' } 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 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 get_client_setup(ip): payload = {'client': [ip]} server = get_server_from_clients([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'] 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') return db_partitions def get_clients(state_filter=None): responses = multi_request('get', '/clients') clients_list = [] for r in responses: clients_list = clients_list + r['json']['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 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 get_allowed_scopes(scopes, allowed_scopes): for scope in scopes.get('scope'): if scope.get('name') in current_user.scopes: allowed_scopes.append(scope) else: get_allowed_scopes(scope, allowed_scopes) 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 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 current_user.scopes: allowed_scopes = [] get_allowed_scopes(all_scopes, allowed_scopes) all_scopes = {'scope': allowed_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 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')) 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: 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')) 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 = (':'.join(mac[i:i+2] for i in range(0, 12, 2))).upper() return jsonify(pretty_mac) @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 r.status_code != requests.codes.ok: flash(_('ogServer: error powering off client'), category='error') else: 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')) @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 scopes, unused = get_scopes(ips) selected_clients = list(get_selected_clients(scopes['scope']).items()) return render_template('actions/select_client.html', selected_clients=selected_clients, form=form, scopes=scopes) @app.route('/action/setup', methods=['GET']) @login_required def action_setup_show(): args = request.args.copy() default_disk = 1 selected_disk = int(args.pop('disk', default_disk)) 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: db_partitions = get_client_setup(base_client) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') if not db_partitions: flash(_('Partition information is not available. Boot client in ogLive mode to obtain it'), category='error') return redirect(url_for('commands')) filtered_partitions = [p for p in db_partitions if p.get('disk') == selected_disk] disk_partition = 0 disks = [d.get('disk') for d in db_partitions if d.get('partition') == disk_partition] form = SetupForm() form.ips.data = ips_str form.disk.data = selected_disk # If partition table is empty, set MSDOS form.disk_type.data = filtered_partitions[0]['code'] or 1 disk_size = filtered_partitions[0]['size'] // 1024 # Make form.partition length equal to (filtered_partitions - 1) length diff = len(filtered_partitions) - 1 - len(form.partitions) [form.partitions.append_entry() for unused in range(diff)] for partition, db_part in zip(form.partitions, filtered_partitions[1:]): partition.partition.data = str(db_part['partition']) partition.part_type.data = db_part['code'] partition.fs.data = db_part['filesystem'] partition.size.data = db_part['size'] // 1024 scopes, _clients = get_scopes(ips) return render_template('actions/setup.html', selected_disk=selected_disk, disks=disks, form=form, disk_size=disk_size, ips=ips_str, base_client=base_client, scopes=scopes) @app.route('/action/setup', methods=['POST']) @login_required def action_setup_modify(): form = SetupForm(request.form) if form.validate(): has_cache=False for partition in form.partitions: if partition.part_type.data == 'CACHE': has_cache=True if not has_cache: flash(_(f'Missing cache partition'), 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': []} required_partitions = ["1", "2", "3", "4"] for partition in form.partitions: print(partition) partition_setup = {'partition': str(partition.partition.data), 'code': str(partition.part_type.data), 'filesystem': str(partition.fs.data), 'size': str(partition.size.data * 1024), 'format': str(int(partition.format_partition.data))} payload['partition_setup'].append(partition_setup) if partition.partition.data in required_partitions: required_partitions.remove(partition.partition.data) if partition.part_type.data == 'CACHE': payload['cache'] = '1' payload['cache_size'] = str(partition.size.data * 1024) for partition in required_partitions: empty_part = { 'partition': partition, 'code': 'EMPTY', 'filesystem': 'EMPTY', 'size': '0', 'format': '0', } payload['partition_setup'].append(empty_part) server = get_server_from_clients(list(ips)) r = server.post('/setup', payload=payload) 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 get_images_grouped_by_repos_from_server(server): r = server.get('/images') if not r: raise ServerError if r.status_code != requests.codes.ok: raise ServerErrorCode images = r.json()['images'] repos={} for image in images: repo_id=image['repo_id'] if repo_id not in repos: repos[repo_id] = [image] else: repos[repo_id].append(image) return repos 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 @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 = form.partition.data.split(' ') 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')) try: repository = get_repository(image['repo_id'], server) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') payload = {'disk': disk, 'partition': partition, 'name': image['name'], 'repository': repository['ip'], 'clients': ips, 'type': form.method.data, 'profile': str(image['software_id']), 'id': str(image['id'])} server.post('/image/restore', payload) 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: ips = parse_elements(request.args.to_dict()) 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_grouped_by_repos_from_server(server) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') if repo_id not in images: flash(_(f'Computer(s) assigned to a repo with no images'), category='error') return redirect(url_for('commands')) for image in images[repo_id]: form.image.choices.append((image['id'], image['name'])) part_choices = [] 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'] 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 part_code = partition['code'] filesystem = partition['filesystem'] choice_value = (disk_id, part_id, part_code, filesystem) parts.append(choice_value) if not part_choices: # Use first computer as reference part setup conf part_choices = [part for part in parts] elif part_choices != parts: flash(_(f'Computers have different partition setup'), category='error') return redirect(url_for('commands')) form.partition.choices = [ (f"{disk_id} {part_id}", f"Disk {disk_id} | Partition {part_id} " f"| {PART_TYPE_CODES[part_code]} " f"{FS_CODES[filesystem]}") for disk_id, part_id, part_code, filesystem in part_choices ] scopes, clients = get_scopes(set(ips)) selected_clients = list(get_selected_clients(scopes['scope']).items()) 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 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 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[part.get('code')]} " f"{FS_CODES[part.get('filesystem')]}") ) 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 = form.os.data.split(' ') server = get_server_from_clients(list(ips)) r = server.post('/session', payload={'clients': ips, 'disk': str(disk), 'partition': str(partition)}) 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()) if not validate_elements(ips, max_len=1): return redirect(url_for('commands')) server = get_server_from_clients(list(ips)) form.ips.data = ' '.join(ips) r = server.get('/session', payload={'client': list(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')) for os in sessions: choice = (f"{os['disk']} {os['partition']}", f"OS: {os['name']} (Disk:{os['disk']}, Partition:{os['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) @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 = 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;"} r = server.get('/images') if not r: return ogserver_down('commands') if r.status_code != requests.codes.ok: return ogserver_error('commands') images = r.json()['images'] ip = list(ips)[0] try: setup = get_client_setup(ip) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') if setup and setup[0].get('code') == 'MSDOS': setup[0]['code'] = 'MBR' for entry in setup: if images and entry['image'] != 0: image = next((img for img in images if img['id'] == entry['image']), None) if image: entry['image'] = image['name'] else: entry['image'] = "" else: entry['image'] = "" scopes, clients = get_scopes(set(ips)) return render_template('actions/client_details.html', form=form, parent="commands.html", scopes=scopes, setup=setup) @app.route('/action/client/update', methods=['GET', 'POST']) @login_required def action_client_update(): form = ClientDetailsForm(request.form) if request.method == 'POST': payload = {"ip": form.ip.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": form.mac.data } server = get_server_from_ip_port(form.server.data) r = server.post('/client/update', payload) 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): flash(_('Please, select one client'), category='error') 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.mac.render_kw = {'readonly': True} form.server.data = "{0}:{1}".format(server.ip, server.port) form.ip.data = db_client['ip'] form.ip.render_kw = {'readonly': True} form.name.data = db_client['name'] form.mac.data = 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 = get_client_setup(ip) except ServerError: return ogserver_down('scopes') except ServerErrorCode: return ogserver_error('scopes') if setup and setup[0].get('code') == 'MSDOS': setup[0]['code'] = 'MBR' r = server.get('/images') if not r: return ogserver_down('scopes') if r.status_code != requests.codes.ok: return ogserver_error('scopes') images = r.json()['images'] for entry in setup: if images and entry['image'] != 0: image = next((img for img in images if img['id'] == entry['image']), None) if image: entry['image'] = image['name'] else: entry['image'] = "" else: entry['image'] = "" form.submit.render_kw = {"formaction": url_for('action_client_update')} return render_template('actions/client_details.html', form=form, parent="scopes.html", scopes=scopes, setup=setup) 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 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/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 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': payload = {"boot": form.boot.data, "ip": form.ip.data, "livedir": form.livedir.data, "mac": form.mac.data, "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 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']] form.boot.choices = list(available_modes) form.mac.render_kw = {'placeholder': 'aabbccddeeaa'} 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_details.html', form=form, parent="scopes.html", scopes=scopes) PLACEHOLDER_TEXT = '''host example1 { hardware ethernet 94:c6:91:a6:25:1a; fixed-address 10.141.10.100; } host example2 { hardware ethernet 94:c6:91:a6:25:1b; fixed-address 10.141.10.101; } host example3 { hardware ethernet 94:c6:91:a6:25:1c; fixed-address 10.141.10.102; } host example4 { hardware ethernet 94:c6:91:a6:25:1d; fixed-address 10.141.10.103; } host example5 { hardware ethernet 94:c6:91:a6:25:1e; fixed-address 10.141.10.104; } host example6 { hardware ethernet 94:c6:91:a6:25:1f; fixed-address 10.141.10.105; } ...''' @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.dhcpd_conf.render_kw = {'placeholder': PLACEHOLDER_TEXT} scopes, _clients = get_scopes() return render_template('actions/import_clients.html', form=form, scopes=scopes) OG_REGEX_DHCPD_CONF = (r'(?:\s*host\s*)' r'([\w.-]*)' r'(?:\s*{[ \n\r]*)' r'(?:\s*hardware *ethernet *)' r'((?:[0-9A-Fa-f]{2}[:-]){5}(?:[0-9A-Fa-f]{2}))' r'(?:\s*;)' r'(?:\s*fixed-address *)' r'(\d+\.\d+\.\d+\.\d+)' r'(?:\s*;)(?:\s*[^}]*})') 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 = re.findall(OG_REGEX_DHCPD_CONF, form.dhcpd_conf.data) if not clients: flash(_('No clients found. Check the dhcpd.conf file.'), 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: payload['name'] = client[0] payload['mac'] = client[1].replace(':', '') payload['ip'] = client[2] resp = server.post('/client/add', payload) if resp.status_code != requests.codes.ok: flash(_('ogServer: error adding client {}').format(client[0]), 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 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')) 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 } print(payload) server = get_server_from_clients(ips) r = server.post('/mode', payload) 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') most_used_mode = max(modes_set, key=lambda m: len(modes_set[m])) available_modes = [] if most_used_mode in r.json()['modes']: available_modes.append((most_used_mode, most_used_mode)) available_modes.extend([(mode, mode) for mode in r.json()['modes'] if mode != most_used_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'] client_name = resp['name'] if oglive not in oglives: oglives[oglive] = [client_name] else: oglives[oglive].append(client_name) 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 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') most_used_oglive = max(oglives_set, key=lambda l: len(oglives_set[l])) available_oglives = [(oglive.get('directory'), oglive.get('directory')) for oglive in r.json()['oglive'] if oglive.get('directory') == most_used_oglive] if not available_oglives: available_oglives.append(('default', 'default')) available_oglives.extend([(oglive.get('directory'), oglive.get('directory')) for oglive in r.json()['oglive'] if oglive.get('directory') != most_used_oglive]) form.oglive.choices = list(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) @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') disk, partition, code = form.os.data.split(' ') payload = {"clients": [ip], "disk": disk, "partition": partition, "code": code, "name": form.name.data, "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 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') for part in r.json()['partitions'][1:]: 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[part.get('code')]} " f"{FS_CODES[part.get('filesystem')]}") ) 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)) return render_template('actions/image_create.html', form=form, scopes=scopes) @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': repository['ip'], '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 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')) ips = parse_elements(request.args.to_dict()) 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_grouped_by_repos_from_server(server) except ServerError: return ogserver_down('commands') except ServerErrorCode: return ogserver_error('commands') if repo_id not in images: flash(_('Computer is assigned to a repo with no images'), category='error') return redirect(url_for('commands')) for image in images[repo_id]: 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') for part in r.json()['partitions'][1:]: 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[part.get('code')]} " f"{FS_CODES[part.get('filesystem')]}") ) 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) @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 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 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 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) 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 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 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 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/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 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_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={} 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'] = 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 = multi_request('get', '/repositories') return render_template('repos.html', repos_resp=responses) @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')) payload = {"name": form.name.data, "ip": form.ip.data, "center": 1} server = get_server_from_ip_port(form.server.data) r = server.post('/repository/add', payload) 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'] responses = multi_request('get', '/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) payload = { 'repo_id': int(form.repo_id.data), 'name': form.name.data, 'ip': form.ip.data, 'center': 1} r = server.post('/repository/update', payload) 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') 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'] form.ip.data = repository['ip'] responses = multi_request('get', '/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': form.repo_id.data } r = server.post('/repository/delete', payload) 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) print(repos) 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') 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.ip.data = repository['ip'] form.ip.render_kw = {'readonly': True} responses = multi_request('get', '/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') 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} form.ip.data = repository['ip'] form.ip.render_kw = {'readonly': True} form.submit.render_kw = {"style": "visibility:hidden;"} responses = multi_request('get', '/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/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('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 {} do not exists').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['name'], center['name']) for center in centers] available_centers.extend(centers) return available_centers def get_available_scopes(): responses = multi_request('get', '/scopes') available_scopes = list() for resp in responses: servers = parse_scopes_from_tree(resp['json'], 'server') servers = [(server['name'], server['name']) for server in servers] available_scopes.extend(servers) centers = parse_scopes_from_tree(resp['json'], 'center') centers = [(center['name'], center['name']) for center in centers] available_scopes.extend(centers) rooms = parse_scopes_from_tree(resp['json'], 'room') rooms = [(room['name'], room['name']) for room in rooms] available_scopes.extend(rooms) 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): username = form.username.data 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 redirect(url_for('users')) admin = form.admin.data scopes = form.scopes.data user = { 'USER': username, 'PASS': pwd_hash, 'ADMIN': admin, 'SCOPES': scopes, } filename = os.path.join(app.root_path, ogcp_cfg_path) with open(filename, 'r+') as file: config = json.load(file) config['USERS'].append(user) file.seek(0) json.dump(config, file, indent='\t') file.truncate() app.config['USERS'].append(user) return redirect(url_for('users')) 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_available_scopes() 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')) return save_user(form) @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 {} do not exists').format(username), category='error') return redirect(url_for('users')) form = UserForm() form.username.data = user.get('USER') form.username.render_kw = {'readonly': True} form.admin.data = user.get('ADMIN') form.scopes.data = user.get('SCOPES') 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 = UserForm(request.form) form.scopes.choices = get_available_scopes() 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 {} do not exists').format(username), category='error') return redirect(url_for('users')) delete_user(username) return save_user(form) @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 {} do not exists').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 an image'), 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 {} do not exists').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'] try: responses = get_images_grouped_by_repos() except ServerError: return ogserver_down('images') except ServerErrorCode: return ogserver_error('images') 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, max_len=1): return redirect(url_for('images')) id = ids.pop() payload = {'image': id} server = get_server_from_ip_port(form.server.data) r = server.post('/image/delete', payload) if r.status_code != requests.codes.ok: flash(_('OgServer replied with a non ok status code'), category='error') else: flash(_('Image deletion request sent 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] 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 = image_id form.server.data = params['image-server'] if not validate_elements(images, max_len=1): flash(_('Please select one image to delete'), category='error') return redirect(url_for('images')) return render_template('actions/delete_image.html', form=form, image_name=image_name.split('_', 1)[0], image_id=image_id, 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)