From 15f3cc3baed5a033a965e89a2c3b14ed378d612f Mon Sep 17 00:00:00 2001 From: leccg Date: Thu, 18 Dec 2025 10:59:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0web/views.py=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E6=8E=A7=E5=88=B6=E5=99=A8=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ctdeployer/web/views.py | 1454 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1454 insertions(+) create mode 100644 ctdeployer/web/views.py diff --git a/ctdeployer/web/views.py b/ctdeployer/web/views.py new file mode 100644 index 0000000..91236bb --- /dev/null +++ b/ctdeployer/web/views.py @@ -0,0 +1,1454 @@ +from builtins import str +from django.http import HttpResponse +from django.http import HttpResponseRedirect +from django.shortcuts import render +from django.views.decorators.csrf import csrf_protect +from django.views.decorators.http import require_POST + +import simplejson +import time +import xmlrpc.client + +import ctdeployer.items.distro as item_distro +import ctdeployer.items.file as item_file +import ctdeployer.items.image as item_image +import ctdeployer.items.mgmtclass as item_mgmtclass +import ctdeployer.items.package as item_package +import ctdeployer.items.profile as item_profile +import ctdeployer.items.repo as item_repo +import ctdeployer.items.system as item_system +import ctdeployer.settings as item_settings +import ctdeployer.utils as utils +from ctdeployer.web import field_ui_info + +url_ctdeployer_api = None +remote = None +username = None + +# ================================================================================== + + +def index(request): + """ + This is the main greeting page for Ctdeployer web. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web", expired=True) + + html = render(request, 'index.tmpl', { + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username, + }) + return HttpResponse(html) + +# ======================================================================== + + +def task_created(request): + """ + Let's the user know what to expect for event updates. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/task_created", expired=True) + + html = render(request, "task_created.tmpl", { + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username + }) + return HttpResponse(html) + +# ======================================================================== + + +def error_page(request, message): + """ + This page is used to explain error messages to the user. + """ + if not test_user_authenticated(request): + return login(request, expired=True) + + # FIXME: test and make sure we use this rather than throwing lots of tracebacks for + # field errors + message = message.replace(":'", "Remote exception: ") + message = message.replace("'\">", "") + + html = render(request, 'error_page.tmpl', { + 'version': remote.extended_version(request.session['token'])['version'], + 'message': message, + 'username': username + }) + return HttpResponse(html) + +# ================================================================================== + + +def _get_field_html_element(field_name): + + if field_name in field_ui_info.USES_SELECT: + return "select" + elif field_name in field_ui_info.USES_MULTI_SELECT: + return "multiselect" + elif field_name in field_ui_info.USES_RADIO: + return "radio" + elif field_name in field_ui_info.USES_CHECKBOX: + return "checkbox" + elif field_name in field_ui_info.USES_TEXTAREA: + return "textarea" + else: + return "text" + + +def get_fields(what, is_subobject, seed_item=None): + + """ + Helper function. Retrieves the field table from the Ctdeployer objects + and formats it in a way to make it useful for Django templating. + The field structure indicates what fields to display and what the default + values are, etc. + """ + + if what == "distro": + fields = item_distro.FIELDS + if what == "profile": + fields = item_profile.FIELDS + if what == "system": + fields = item_system.FIELDS + if what == "repo": + fields = item_repo.FIELDS + if what == "image": + fields = item_image.FIELDS + if what == "mgmtclass": + fields = item_mgmtclass.FIELDS + if what == "package": + fields = item_package.FIELDS + if what == "file": + fields = item_file.FIELDS + if what == "setting": + fields = item_settings.FIELDS + + settings = remote.get_settings() + + ui_fields = [] + for field in fields: + + ui_field = { + "name": field[0], + "dname": field[0], + "value": "?", + "caption": field[3], + "editable": field[4], + "tooltip": field[5], + "choices": field[6], + "css_class": "generic", + "html_ui_fieldent": "generic", + } + + if not ui_field["editable"]: + continue + + name = field[0] + if seed_item is not None: + ui_field["value"] = seed_item[name] + elif is_subobject: + ui_field["value"] = field[2] + else: + ui_field["value"] = field[1] + + if ui_field["value"] is None: + ui_field["value"] = "" + + # we'll process this for display but still need to present the original to some + # template logic + ui_field["value_raw"] = ui_field["value"] + + if isinstance(ui_field["value"], str) and ui_field["value"].startswith("SETTINGS:"): + key = ui_field["value"].replace("SETTINGS:", "", 1) + ui_field["value"] = settings[key] + + # flatten dicts of all types, they can only be edited as text + # as we have no HTML dict widget (yet) + if isinstance(ui_field["value"], dict): + if ui_field["name"] == "mgmt_parameters": + # Render dictionary as YAML for Management Parameters field + tokens = [] + for (x, y) in list(ui_field["value"].items()): + if y is not None: + tokens.append("%s: %s" % (x, y)) + else: + tokens.append("%s: " % x) + ui_field["value"] = "{ %s }" % ", ".join(tokens) + else: + tokens = [] + for (x, y) in list(ui_field["value"].items()): + if isinstance(y, str) and y.strip() != "~": + y = y.replace(" ", "\\ ") + tokens.append("%s=%s" % (x, y)) + elif isinstance(y, list): + for item in y: + item = item.replace(" ", "\\ ") + tokens.append("%s=%s" % (x, item)) + elif y is not None: + tokens.append("%s" % x) + ui_field["value"] = " ".join(tokens) + + name = field[0] + ui_field["html_element"] = _get_field_html_element(name) + + # flatten lists for those that aren't using select boxes + if isinstance(ui_field["value"], list): + if ui_field["html_element"] != "select": + ui_field["value"] = " ".join(ui_field["value"]) + + ui_fields.append(ui_field) + + return ui_fields + + +def get_network_interface_fields(): + """ + Create network interface fields UI metadata based on network interface + fields metadata + + @return list network interface fields UI metadata + """ + + fields = item_system.NETWORK_INTERFACE_FIELDS + + fields_ui = [] + for field in fields: + + field_ui = { + "name": field[0], + "dname": field[0], + "value": "?", + "caption": field[3], + "editable": field[4], + "tooltip": field[5], + "choices": field[6], + "css_class": "generic", + "html_element": "generic", + } + + if not field_ui["editable"]: + continue + + # system's network interfaces are loaded later by javascript, + # initial value on web UI is always empty string + field_ui["value"] = "" + + # we'll process this for display but still need to present the original + # to some template logic + field_ui["value_raw"] = field_ui["value"] + + name = field[0] + field_ui["html_element"] = _get_field_html_element(name) + + fields_ui.append(field_ui) + + return fields_ui + + +def _create_sections_metadata(what, sections_data, fields): + + sections = {} + section_index = 0 + for section_data in sections_data: + for section_name, section_fields in list(section_data.items()): + skey = "%d_%s" % (section_index, section_name) + sections[skey] = {} + sections[skey]['name'] = section_name + sections[skey]['fields'] = [] + + for section_field in section_fields: + found = False + for field in fields: + if field["name"] == section_field: + sections[skey]['fields'].append(field) + found = True + break + if not found: + raise Exception("%s field %s referenced in UI section definition does not exist in UI fields definition" % (what, section_field)) + + section_index += 1 + + return sections + +# ================================================================================== + + +def __tweak_field(fields, field_name, attribute, value): + """ + Helper function to insert extra data into the field list. + """ + # FIXME: eliminate this function. + for x in fields: + if x["name"] == field_name: + x[attribute] = value + +# ================================================================================== + + +def __format_columns(column_names, sort_field): + """ + Format items retrieved from XMLRPC for rendering by the generic_edit template + """ + dataset = [] + + # Default is sorting on name + if sort_field is not None: + sort_name = sort_field + else: + sort_name = "name" + + if sort_name.startswith("!"): + sort_name = sort_name[1:] + sort_order = "desc" + else: + sort_order = "asc" + + for fieldname in column_names: + fieldorder = "none" + if fieldname == sort_name: + fieldorder = sort_order + dataset.append([fieldname, fieldorder]) + return dataset + + +# ================================================================================== + + +def __format_items(items, column_names): + """ + Format items retrieved from XMLRPC for rendering by the generic_edit template + """ + dataset = [] + for item_dict in items: + row = [] + for fieldname in column_names: + if fieldname == "name": + html_element = "name" + elif fieldname in ["system", "repo", "distro", "profile", "image", "mgmtclass", "package", "file"]: + html_element = "editlink" + elif fieldname in field_ui_info.USES_CHECKBOX: + html_element = "checkbox" + else: + html_element = "text" + row.append([fieldname, item_dict[fieldname], html_element]) + dataset.append(row) + return dataset + +# ================================================================================== + + +def genlist(request, what, page=None): + """ + Lists all object types, complete with links to actions + on those objects. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/%s/list" % what, expired=True) + + # get details from the session + if page is None: + page = int(request.session.get("%s_page" % what, 1)) + limit = int(request.session.get("%s_limit" % what, 50)) + sort_field = request.session.get("%s_sort_field" % what, "name") + filters = simplejson.loads(request.session.get("%s_filters" % what, "{}")) + pageditems = remote.find_items_paged(what, utils.strip_none(filters), sort_field, page, limit) + + # what columns to show for each page? + # we also setup the batch actions here since they're dependent + # on what we're looking at + + profiles = [] + + # everythng gets batch delete + batchactions = [ + ["Delete", "delete", "delete"], + ] + + if what == "distro": + columns = ["name"] + batchactions += [ + ["Build ISO", "buildiso", "enable"], + ] + if what == "profile": + columns = ["name", "distro"] + batchactions += [ + ["Build ISO", "buildiso", "enable"], + ] + if what == "system": + # FIXME: also list network, once working + columns = ["name", "profile", "status", "netboot_enabled"] + profiles = sorted(remote.get_profiles(), key=lambda x: x['name']) + batchactions += [ + ["Power on", "power", "on"], + ["Power off", "power", "off"], + ["Reboot", "power", "reboot"], + ["Change profile", "profile", ""], + ["Netboot enable", "netboot", "enable"], + ["Netboot disable", "netboot", "disable"], + ["Build ISO", "buildiso", "enable"], + ] + if what == "repo": + columns = ["name", "mirror"] + batchactions += [ + ["Reposync", "reposync", "go"], + ] + if what == "image": + columns = ["name", "file"] + if what == "network": + columns = ["name"] + if what == "mgmtclass": + columns = ["name"] + if what == "package": + columns = ["name", "installer"] + if what == "file": + columns = ["name"] + + # render the list + html = render(request, 'generic_list.tmpl', { + 'what': what, + 'columns': __format_columns(columns, sort_field), + 'items': __format_items(pageditems["items"], columns), + 'pageinfo': pageditems["pageinfo"], + 'filters': filters, + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username, + 'limit': limit, + 'batchactions': batchactions, + 'profiles': profiles, + }) + return HttpResponse(html) + + +@require_POST +@csrf_protect +def modify_list(request, what, pref, value=None): + """ + This function is used in the generic list view + to modify the page/column sort/number of items + shown per page, and also modify the filters. + + This function modifies the session object to + store these preferences persistently. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/%s/modifylist/%s/%s" % (what, pref, str(value)), expired=True) + + # what preference are we tweaking? + + if pref == "sort": + + # FIXME: this isn't exposed in the UI. + + # sorting list on columns + old_sort = request.session.get("%s_sort_field" % what, "name") + if old_sort.startswith("!"): + old_sort = old_sort[1:] + old_revsort = True + else: + old_revsort = False + # User clicked on the column already sorted on, + # so reverse the sorting list + if old_sort == value and not old_revsort: + value = "!" + value + request.session["%s_sort_field" % what] = value + request.session["%s_page" % what] = 1 + + elif pref == "limit": + # number of items to show per page + request.session["%s_limit" % what] = int(value) + request.session["%s_page" % what] = 1 + + elif pref == "page": + # what page are we currently on + request.session["%s_page" % what] = int(value) + + elif pref in ("addfilter", "removefilter"): + # filters limit what we show in the lists + # they are stored in json format for marshalling + filters = simplejson.loads(request.session.get("%s_filters" % what, "{}")) + if pref == "addfilter": + (field_name, field_value) = value.split(":", 1) + # add this filter + filters[field_name] = field_value + else: + # remove this filter, if it exists + if value in filters: + del filters[value] + # save session variable + request.session["%s_filters" % what] = simplejson.dumps(filters) + # since we changed what is viewed, reset the page + request.session["%s_page" % what] = 1 + + else: + return error_page(request, "Invalid preference change request") + + # redirect to the list page + return HttpResponseRedirect("/ctdeployer_web/%s/list" % what) + +# ====================================================================== + + +@require_POST +@csrf_protect +def generic_rename(request, what, obj_name=None, obj_newname=None): + """ + Renames an object. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/%s/rename/%s/%s" % (what, obj_name, obj_newname), expired=True) + + if obj_name is None: + return error_page(request, "You must specify a %s to rename" % what) + if not remote.has_item(what, obj_name): + return error_page(request, "Unknown %s specified" % what) + elif not remote.check_access_no_fail(request.session['token'], "modify_%s" % what, obj_name): + return error_page(request, "You do not have permission to rename this %s" % what) + else: + obj_id = remote.get_item_handle(what, obj_name, request.session['token']) + remote.rename_item(what, obj_id, obj_newname, request.session['token']) + return HttpResponseRedirect("/ctdeployer_web/%s/list" % what) + +# ====================================================================== + + +@require_POST +@csrf_protect +def generic_copy(request, what, obj_name=None, obj_newname=None): + """ + Copies an object. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/%s/copy/%s/%s" % (what, obj_name, obj_newname), expired=True) + # FIXME: shares all but one line with rename, merge it. + if obj_name is None: + return error_page(request, "You must specify a %s to rename" % what) + if not remote.has_item(what, obj_name): + return error_page(request, "Unknown %s specified" % what) + elif not remote.check_access_no_fail(request.session['token'], "modify_%s" % what, obj_name): + return error_page(request, "You do not have permission to copy this %s" % what) + else: + obj_id = remote.get_item_handle(what, obj_name, request.session['token']) + remote.copy_item(what, obj_id, obj_newname, request.session['token']) + return HttpResponseRedirect("/ctdeployer_web/%s/list" % what) + +# ====================================================================== + + +@require_POST +@csrf_protect +def generic_delete(request, what, obj_name=None): + """ + Deletes an object. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/%s/delete/%s" % (what, obj_name), expired=True) + # FIXME: consolidate code with above functions. + if obj_name is None: + return error_page(request, "You must specify a %s to delete" % what) + if not remote.has_item(what, obj_name): + return error_page(request, "Unknown %s specified" % what) + elif not remote.check_access_no_fail(request.session['token'], "remove_%s" % what, obj_name): + return error_page(request, "You do not have permission to delete this %s" % what) + else: + # check whether object is to be deleted recursively + recursive = simplejson.loads(request.POST.get("recursive", "false")) + try: + remote.xapi_object_edit(what, obj_name, "remove", {'name': obj_name, 'recursive': recursive}, request.session['token']) + except Exception as e: + return error_page(request, str(e)) + return HttpResponseRedirect("/ctdeployer_web/%s/list" % what) + + +# ====================================================================== + + +@require_POST +@csrf_protect +def generic_domulti(request, what, multi_mode=None, multi_arg=None): + """ + Process operations like profile reassignment, netboot toggling, and deletion + which occur on all items that are checked on the list page. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/%s/multi/%s/%s" % (what, multi_mode, multi_arg), expired=True) + + names = request.POST.get('names', '').strip().split() + if names == "": + return error_page(request, "Need to select some '%s' objects first" % what) + + if multi_mode == "delete": + # check whether the objects are to be deleted recursively + recursive = simplejson.loads(request.POST.get("recursive_batch", "false")) + for obj_name in names: + try: + remote.xapi_object_edit(what, obj_name, "remove", {'name': obj_name, 'recursive': recursive}, request.session['token']) + except Exception as e: + return error_page(request, str(e)) + + elif what == "system" and multi_mode == "netboot": + netboot_enabled = multi_arg # values: enable or disable + if netboot_enabled is None: + return error_page(request, "Cannot modify systems without specifying netboot_enabled") + if netboot_enabled == "enable": + netboot_enabled = True + elif netboot_enabled == "disable": + netboot_enabled = False + else: + return error_page(request, "Invalid netboot option, expect enable or disable") + for obj_name in names: + obj_id = remote.get_system_handle(obj_name, request.session['token']) + remote.modify_system(obj_id, "netboot_enabled", netboot_enabled, request.session['token']) + remote.save_system(obj_id, request.session['token'], "edit") + + elif what == "system" and multi_mode == "profile": + profile = multi_arg + if profile is None: + return error_page(request, "Cannot modify systems without specifying profile") + for obj_name in names: + obj_id = remote.get_system_handle(obj_name, request.session['token']) + remote.modify_system(obj_id, "profile", profile, request.session['token']) + remote.save_system(obj_id, request.session['token'], "edit") + + elif what == "system" and multi_mode == "power": + power = multi_arg + if power is None: + return error_page(request, "Cannot modify systems without specifying power option") + options = {"systems": names, "power": power} + remote.background_power_system(options, request.session['token']) + + elif what == "system" and multi_mode == "buildiso": + options = {"systems": names, "profiles": []} + remote.background_buildiso(options, request.session['token']) + + elif what == "profile" and multi_mode == "buildiso": + options = {"profiles": names, "systems": []} + remote.background_buildiso(options, request.session['token']) + + elif what == "distro" and multi_mode == "buildiso": + if len(names) > 1: + return error_page(request, "You can only select one distro at a time to build an ISO for") + options = {"standalone": True, "distro": str(names[0])} + remote.background_buildiso(options, request.session['token']) + + elif what == "repo" and multi_mode == "reposync": + options = {"repos": names, "tries": 3} + remote.background_reposync(options, request.session['token']) + + else: + return error_page(request, "Unknown batch operation on %ss: %s" % (what, str(multi_mode))) + + # FIXME: "operation complete" would make a lot more sense here than a redirect + return HttpResponseRedirect("/ctdeployer_web/%s/list" % what) + +# ====================================================================== + + +def import_prompt(request): + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/import/prompt", expired=True) + + html = render(request, 'import.tmpl', { + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username, + }) + return HttpResponse(html) + +# ====================================================================== + + +def check(request): + """ + Shows a page with the results of 'ctdeployer check' + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/check", expired=True) + + results = remote.check(request.session['token']) + + html = render(request, 'check.tmpl', { + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username, + 'results': results + }) + return HttpResponse(html) + +# ====================================================================== + + +@require_POST +@csrf_protect +def buildiso(request): + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/buildiso", expired=True) + remote.background_buildiso({}, request.session['token']) + return HttpResponseRedirect('/ctdeployer_web/task_created') + +# ====================================================================== + + +@require_POST +@csrf_protect +def import_run(request): + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/import/prompt", expired=True) + options = { + "name": request.POST.get("name", ""), + "path": request.POST.get("path", ""), + "breed": request.POST.get("breed", ""), + "arch": request.POST.get("arch", "") + } + remote.background_import(options, request.session['token']) + return HttpResponseRedirect('/ctdeployer_web/task_created') + +# ====================================================================== + + +def aifile_list(request, page=None): + """ + List all automatic OS installation templates and link to their edit pages. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/aifile/list", expired=True) + + aifiles = remote.get_autoinstall_templates(request.session['token']) + + aifile_list = [] + for aifile in aifiles: + aifile_list.append((aifile, 'editable')) + + html = render(request, 'aifile_list.tmpl', { + 'what': 'aifile', + 'ai_files': aifile_list, + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username, + 'item_count': len(aifile_list[0]), + }) + return HttpResponse(html) + +# ====================================================================== + + +@csrf_protect +def aifile_edit(request, aifile_name=None, editmode='edit'): + """ + This is the page where an automatic OS installation file is edited. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/aifile/edit/file:%s" % aifile_name, expired=True) + if editmode == 'edit': + editable = False + else: + editable = True + deleteable = False + aidata = "" + if aifile_name is not None: + editable = remote.check_access_no_fail(request.session['token'], "modify_autoinst", aifile_name) + deleteable = not remote.is_autoinstall_in_use(aifile_name, request.session['token']) + aidata = remote.read_autoinstall_template(aifile_name, request.session['token']) + + html = render(request, 'aifile_edit.tmpl', { + 'aifile_name': aifile_name, + 'deleteable': deleteable, + 'aidata': aidata, + 'editable': editable, + 'editmode': editmode, + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username + }) + return HttpResponse(html) + +# ====================================================================== + + +@require_POST +@csrf_protect +def aifile_save(request): + """ + This page processes and saves edits to an automatic OS installation file. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/aifile/list", expired=True) + # FIXME: error checking + + aifile_name = request.POST.get('aifile_name', None) + aidata = request.POST.get('aidata', "").replace('\r\n', '\n') + + if aifile_name is None: + return HttpResponse("NO AUTOMATIC INSTALLATION FILE NAME SPECIFIED") + + delete1 = request.POST.get('delete1', None) + delete2 = request.POST.get('delete2', None) + + if delete1 and delete2: + remote.remove_autoinstall_template(aifile_name, request.session['token']) + return HttpResponseRedirect('/ctdeployer_web/aifile/list') + else: + remote.write_autoinstall_template(aifile_name, aidata, request.session['token']) + return HttpResponseRedirect('/ctdeployer_web/aifile/list') + +# ====================================================================== + + +def snippet_list(request, page=None): + """ + This page lists all available snippets and has links to edit them. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/snippet/list", expired=True) + + snippets = remote.get_autoinstall_snippets(request.session['token']) + snippet_list = [] + for snippet in snippets: + snippet_list.append((snippet, 'editable')) + + html = render(request, 'snippet_list.tmpl', { + 'what': 'snippet', + 'snippets': snippet_list, + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username + }) + return HttpResponse(html) + +# ====================================================================== + + +@csrf_protect +def snippet_edit(request, snippet_name=None, editmode='edit'): + """ + This page edits a specific snippet. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/edit/file:%s" % snippet_name, expired=True) + if editmode == 'edit': + editable = False + else: + editable = True + deleteable = False + snippetdata = "" + if snippet_name is not None: + editable = remote.check_access_no_fail(request.session['token'], "modify_snippet", snippet_name) + deleteable = True + snippetdata = remote.read_autoinstall_snippet(snippet_name, request.session['token']) + + html = render(request, 'snippet_edit.tmpl', { + 'snippet_name': snippet_name, + 'deleteable': deleteable, + 'snippetdata': snippetdata, + 'editable': editable, + 'editmode': editmode, + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username + }) + return HttpResponse(html) + +# ====================================================================== + + +@require_POST +@csrf_protect +def snippet_save(request): + """ + This snippet saves a snippet once edited. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/snippet/list", expired=True) + # FIXME: error checking + + editmode = request.POST.get('editmode', 'edit') + snippet_name = request.POST.get('snippet_name', None) + snippetdata = request.POST.get('snippetdata', "").replace('\r\n', '\n') + + if snippet_name is None: + return HttpResponse("NO SNIPPET NAME SPECIFIED") + + if editmode != 'edit': + if snippet_name.find("/var/lib/ctdeployer/snippets/") != 0: + snippet_name = "/var/lib/ctdeployer/snippets/" + snippet_name + + delete1 = request.POST.get('delete1', None) + delete2 = request.POST.get('delete2', None) + + if delete1 and delete2: + remote.remove_autoinstall_snippet(snippet_name, request.session['token']) + return HttpResponseRedirect('/ctdeployer_web/snippet/list') + else: + remote.write_autoinstall_snippet(snippet_name, snippetdata, request.session['token']) + return HttpResponseRedirect('/ctdeployer_web/snippet/list') + +# ====================================================================== + + +def setting_list(request): + """ + This page presents a list of all the settings to the user. They are not editable. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/setting/list", expired=True) + settings = remote.get_settings() + skeys = list(settings.keys()) + skeys.sort() + + results = [] + for k in skeys: + results.append([k, settings[k]]) + + html = render(request, 'settings.tmpl', { + 'settings': results, + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username, + }) + return HttpResponse(html) + + +@csrf_protect +def setting_edit(request, setting_name=None): + if not setting_name: + return HttpResponseRedirect('/ctdeployer_web/setting/list') + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/setting/edit/%s" % setting_name, expired=True) + + settings = remote.get_settings() + if setting_name not in settings: + return error_page(request, "Unknown setting: %s" % setting_name) + + cur_setting = { + 'name': setting_name, + 'value': settings[setting_name], + } + + fields = get_fields('setting', False, seed_item=cur_setting) + + # build UI tabs metadata + sections_data = field_ui_info.SETTING_UI_FIELDS_MAPPING + sections = _create_sections_metadata('setting', sections_data, fields) + + html = render(request, 'generic_edit.tmpl', { + 'what': 'setting', + 'sections': sections, + 'subobject': False, + 'editmode': 'edit', + 'editable': True, + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username, + 'name': setting_name, + }) + return HttpResponse(html) + + +@csrf_protect +def setting_save(request): + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/setting/list", expired=True) + + # load request fields and see if they are valid + setting_name = request.POST.get('name', "") + setting_value = request.POST.get('value', None) + + if setting_name == "": + return error_page(request, "The setting name was not specified") + + settings = remote.get_settings() + if setting_name not in settings: + return error_page(request, "Unknown setting: %s" % setting_name) + + if remote.modify_setting(setting_name, setting_value, request.session['token']): + return error_page(request, "There was an error saving the setting") + + return HttpResponseRedirect("/ctdeployer_web/setting/list") + +# ====================================================================== + + +def events(request): + """ + This page presents a list of all the events and links to the event log viewer. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/events", expired=True) + events = remote.get_events() + + events2 = [] + for id in list(events.keys()): + (ttime, name, state, read_by) = events[id] + events2.append([id, time.asctime(time.localtime(ttime)), name, state]) + + events2 = sorted(events2) + + html = render(request, 'events.tmpl', { + 'results': events2, + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username + }) + return HttpResponse(html) + +# ====================================================================== + + +def eventlog(request, event=0): + """ + Shows the log for a given event. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/eventlog/%s" % str(event), expired=True) + event_info = remote.get_events() + if event not in event_info: + return HttpResponse("event not found") + + data = event_info[event] + eventname = data[0] + eventtime = data[1] + eventstate = data[2] + eventlog = remote.get_event_log(event) + + html = render(request, 'eventlog.tmpl', { + 'eventlog': eventlog, + 'eventname': eventname, + 'eventstate': eventstate, + 'eventid': event, + 'eventtime': eventtime, + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username + }) + return HttpResponse(html) + +# ====================================================================== + + +def random_mac(request, virttype="xenpv"): + """ + Used in an ajax call to fill in a field with a mac address. + """ + # FIXME: not exposed in UI currently + if not test_user_authenticated(request): + return login(request, expired=True) + random_mac = remote.get_random_mac(virttype, request.session['token']) + return HttpResponse(random_mac) + +# ====================================================================== + + +@require_POST +@csrf_protect +def sync(request): + """ + Runs 'ctdeployer sync' from the API when the user presses the sync button. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/sync", expired=True) + remote.background_sync({"verbose": "True"}, request.session['token']) + return HttpResponseRedirect("/ctdeployer_web/task_created") + +# ====================================================================== + + +@require_POST +@csrf_protect +def reposync(request): + """ + Syncs all repos that are configured to be synced. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/reposync", expired=True) + remote.background_reposync({"names": "", "tries": 3}, request.session['token']) + return HttpResponseRedirect("/ctdeployer_web/task_created") + +# ====================================================================== + + +@require_POST +@csrf_protect +def hardlink(request): + """ + Hardlinks files between repos and install trees to save space. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/hardlink", expired=True) + remote.background_hardlink({}, request.session['token']) + return HttpResponseRedirect("/ctdeployer_web/task_created") + +# ====================================================================== + + +@require_POST +@csrf_protect +def replicate(request): + """ + Replicate configuration from the central Ctdeployer server, configured + in /etc/ctdeployer/settings (note: this is uni-directional!) + + FIXME: this is disabled because we really need a web page to provide options for + this command. + + """ + # settings = remote.get_settings() + # options = settings # just load settings from file until we decide to ask user (later?) + # remote.background_replicate(options, request.session['token']) + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/replicate", expired=True) + return HttpResponseRedirect("/ctdeployer_web/task_created") + +# ====================================================================== + + +def __names_from_dicts(lod, optional=True): + """ + Tiny helper function. + Get the names out of an array of dictionaries that the remote interface + returns. + """ + results = [] + if optional: + results.append("<>") + for x in lod: + results.append(x["name"]) + results.sort() + return results + +# ====================================================================== + + +@csrf_protect +def generic_edit(request, what=None, obj_name=None, editmode="new"): + """ + Presents an editor page for any type of object. + While this is generally standardized, systems are a little bit special. + """ + target = "" + if obj_name is not None: + target = "/%s" % obj_name + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/%s/edit%s" % (what, target), expired=True) + + obj = None + + child = False + if what == "subprofile": + what = "profile" + child = True + + if obj_name is not None: + editable = remote.check_access_no_fail(request.session['token'], "modify_%s" % what, obj_name) + obj = remote.get_item(what, obj_name, False) + else: + editable = remote.check_access_no_fail(request.session['token'], "new_%s" % what, None) + obj = None + + interfaces = {} + if what == "system": + if obj: + interfaces = obj.get("interfaces", {}) + else: + interfaces = {} + + fields = get_fields(what, child, obj) + if what == "system": + fields += get_network_interface_fields() + + # create the autoinstall pulldown list + autoinstalls = remote.get_autoinstall_templates() + autoinstall_list = ["", "<>"] + for autoinstall in autoinstalls: + autoinstall_list.append(autoinstall) + + # populate some select boxes + if what == "profile": + if (obj and obj["parent"] not in (None, "")) or child: + __tweak_field(fields, "parent", "choices", __names_from_dicts(remote.get_profiles())) + else: + __tweak_field(fields, "distro", "choices", __names_from_dicts(remote.get_distros())) + __tweak_field(fields, "autoinstall", "choices", autoinstall_list) + __tweak_field(fields, "repos", "choices", __names_from_dicts(remote.get_repos())) + __tweak_field(fields, "mgmt_classes", "choices", __names_from_dicts(remote.get_mgmtclasses(), optional=False)) + + elif what == "system": + __tweak_field(fields, "profile", "choices", __names_from_dicts(remote.get_profiles())) + __tweak_field(fields, "image", "choices", __names_from_dicts(remote.get_images(), optional=True)) + __tweak_field(fields, "autoinstall", "choices", autoinstall_list) + __tweak_field(fields, "mgmt_classes", "choices", __names_from_dicts(remote.get_mgmtclasses(), optional=False)) + + elif what == "mgmtclass": + __tweak_field(fields, "packages", "choices", __names_from_dicts(remote.get_packages())) + __tweak_field(fields, "files", "choices", __names_from_dicts(remote.get_files())) + + elif what == "distro": + __tweak_field(fields, "arch", "choices", remote.get_valid_archs()) + __tweak_field(fields, "os_version", "choices", remote.get_valid_os_versions()) + __tweak_field(fields, "breed", "choices", remote.get_valid_breeds()) + __tweak_field(fields, "mgmt_classes", "choices", __names_from_dicts(remote.get_mgmtclasses(), optional=False)) + + elif what == "image": + __tweak_field(fields, "arch", "choices", remote.get_valid_archs()) + __tweak_field(fields, "breed", "choices", remote.get_valid_breeds()) + __tweak_field(fields, "os_version", "choices", remote.get_valid_os_versions()) + __tweak_field(fields, "autoinst", "choices", autoinstall_list) + + # if editing save the fields in the session for comparison later + if editmode == "edit": + request.session['%s_%s' % (what, obj_name)] = fields + + # build UI tabs metadata + if what == "distro": + sections_data = field_ui_info.DISTRO_UI_FIELDS_MAPPING + elif what == "file": + sections_data = field_ui_info.FILE_UI_FIELDS_MAPPING + elif what == "image": + sections_data = field_ui_info.IMAGE_UI_FIELDS_MAPPING + elif what == "mgmtclass": + sections_data = field_ui_info.MGMTCLASS_UI_FIELDS_MAPPING + elif what == "package": + sections_data = field_ui_info.PACKAGE_UI_FIELDS_MAPPING + elif what == "profile": + sections_data = field_ui_info.PROFILE_UI_FIELDS_MAPPING + elif what == "repo": + sections_data = field_ui_info.REPO_UI_FIELDS_MAPPING + elif what == "system": + sections_data = field_ui_info.SYSTEM_UI_FIELDS_MAPPING + sections = _create_sections_metadata(what, sections_data, fields) + + inames = list(interfaces.keys()) + inames.sort() + + html = render(request, 'generic_edit.tmpl', { + 'what': what, + 'sections': sections, + 'subobject': child, + 'editmode': editmode, + 'editable': editable, + 'interfaces': interfaces, + 'interface_names': inames, + 'interface_length': len(inames), + 'version': remote.extended_version(request.session['token'])['version'], + 'username': username, + 'name': obj_name + }) + return HttpResponse(html) + +# ====================================================================== + + +@require_POST +@csrf_protect +def generic_save(request, what): + """ + Saves an object back using the Ctdeployer API after clearing any 'generic_edit' page. + """ + if not test_user_authenticated(request): + return login(request, next="/ctdeployer_web/%s/list" % what, expired=True) + + # load request fields and see if they are valid + editmode = request.POST.get('editmode', 'edit') + obj_name = request.POST.get('name', "") + subobject = request.POST.get('subobject', "False") + + if subobject == "False": + subobject = False + else: + subobject = True + + if obj_name == "": + return error_page(request, "Required field name is missing") + + prev_fields = [] + if "%s_%s" % (what, obj_name) in request.session and editmode == "edit": + prev_fields = request.session["%s_%s" % (what, obj_name)] + + # grab the remote object handle + # for edits, fail in the object cannot be found to be edited + # for new objects, fail if the object already exists + if editmode == "edit": + if not remote.has_item(what, obj_name): + return error_page(request, "Failure trying to access item %s, it may have been deleted." % (obj_name)) + obj_id = remote.get_item_handle(what, obj_name, request.session['token']) + else: + if remote.has_item(what, obj_name): + return error_page(request, "Could not create a new item %s, it already exists." % (obj_name)) + obj_id = remote.new_item(what, request.session['token']) + + # system needs either profile or image to be set + # fail if both are not set + if what == "system": + profile = request.POST.getlist('profile') + image = request.POST.getlist('image') + if "<>" in profile and "<>" in image: + return error_page(request, "Please provide either a valid profile or image for the system") + + # walk through our fields list saving things we know how to save + fields = get_fields(what, subobject) + + for field in fields: + + if field['name'] == 'name' and editmode == 'edit': + # do not attempt renames here + continue + else: + # check and see if the value exists in the fields stored in the session + prev_value = None + for prev_field in prev_fields: + if prev_field['name'] == field['name']: + prev_value = prev_field['value'] + break + + value = request.POST.get(field['name'], None) + # Checkboxes return the value of the field if checked, otherwise None + # convert to True/False + if field["html_element"] == "checkbox": + if value == field['name']: + value = True + else: + value = False + + # Multiselect fields are handled differently + if field["html_element"] == "multiselect": + values = request.POST.getlist(field['name']) + value = [] + if '<>' in values: + value = '<>' + else: + for single_value in values: + if single_value != "<>": + value.insert(0, single_value) + + if value is not None: + if value == "<>": + value = "" + if value is not None and (not subobject or field['name'] != 'distro') and value != prev_value: + try: + remote.modify_item(what, obj_id, field['name'], value, request.session['token']) + except Exception as e: + return error_page(request, str(e)) + + # special handling for system interface fields + # which are the only objects in Ctdeployer that will ever work this way + if what == "system": + network_interface_fields = get_network_interface_fields() + interfaces = request.POST.get('interface_list', "").split(",") + for interface in interfaces: + if interface == "": + continue + ifdata = {} + for field in network_interface_fields: + ifdata["%s-%s" % (field["name"], interface)] = request.POST.get("%s-%s" % (field["name"], interface), "") + ifdata = utils.strip_none(ifdata) + # FIXME: I think this button is missing. + present = request.POST.get("present-%s" % interface, "") + original = request.POST.get("original-%s" % interface, "") + try: + if present == "0" and original == "1": + remote.modify_system(obj_id, 'delete_interface', interface, request.session['token']) + elif present == "1": + remote.modify_system(obj_id, 'modify_interface', ifdata, request.session['token']) + except Exception as e: + return error_page(request, str(e)) + + try: + remote.save_item(what, obj_id, request.session['token'], editmode) + except Exception as e: + return error_page(request, str(e)) + + return HttpResponseRedirect('/ctdeployer_web/%s/list' % what) + + +# ====================================================================== +# Login/Logout views + +def test_user_authenticated(request): + global remote + global username + global url_ctdeployer_api + + if url_ctdeployer_api is None: + url_ctdeployer_api = utils.local_get_ctdeployer_api_url() + + remote = xmlrpc.client.Server(url_ctdeployer_api, allow_none=True) + + # if we have a token, get the associated username from + # the remote server via XMLRPC. We then compare that to + # the value stored in the session. If everything matches up, + # the user is considered successfully authenticated + if 'token' in request.session and request.session['token'] != '': + try: + if remote.token_check(request.session['token']): + token_user = remote.get_user_from_token(request.session['token']) + if 'username' in request.session and request.session['username'] == token_user: + username = request.session['username'] + return True + except: + # just let it fall through to the 'return False' below + pass + return False + + +use_passthru = -1 + + +@csrf_protect +def login(request, next=None, message=None, expired=False): + global use_passthru + if use_passthru < 0: + token = remote.login("", utils.get_shared_secret()) + auth_module = remote.get_authn_module_name(token) + use_passthru = auth_module == 'authentication.passthru' + + if use_passthru: + return accept_remote_user(request, next) + + if expired and not message: + message = "Sorry, either you need to login or your session expired." + return render(request, 'login.tmpl', {'next': next, 'message': message}) + + +def accept_remote_user(request, nextsite): + global username + + username = request.META['REMOTE_USER'] + token = remote.login(username, utils.get_shared_secret()) + + request.session['username'] = username + request.session['token'] = token + if nextsite: + return HttpResponseRedirect(nextsite) + else: + return HttpResponseRedirect("/ctdeployer_web") + + +@require_POST +@csrf_protect +def do_login(request): + global remote + global username + global url_ctdeployer_api + + username = request.POST.get('username', '').strip() + password = request.POST.get('password', '') + nextsite = request.POST.get('next', None) + + if url_ctdeployer_api is None: + url_ctdeployer_api = utils.local_get_ctdeployer_api_url() + + remote = xmlrpc.client.Server(url_ctdeployer_api, allow_none=True) + + try: + token = remote.login(username, password) + except: + token = None + + if token: + request.session['username'] = username + request.session['token'] = token + if nextsite: + return HttpResponseRedirect(nextsite) + else: + return HttpResponseRedirect("/ctdeployer_web") + else: + return login(request, nextsite, message="Login failed, please try again") + + +@require_POST +@csrf_protect +def do_logout(request): + request.session['username'] = "" + request.session['token'] = "" + return HttpResponseRedirect("/ctdeployer_web") -- Gitee