/* design-a-pod.js Functions as the "controller" part of MVC */ const steps = { SELECT_LAB: 0, ADD_RESOURCES: 1, ADD_NETWORKS: 2, CONFIGURE_CONNECTIONS: 3, POD_DETAILS: 4, POD_SUMMARY: 5 } /** Concrete controller class that handles button inputs from the user. * Holds the in-progress TemplateBlob. * Click methods are prefaced with 'onclick'. * Step initialization methods are prefaced with 'step'. */ class DesignWorkflow extends Workflow { constructor(savedTemplateBlob) { super(["select_lab", "add_resources", "add_networks", "configure_connections", "pod_details", "pod_summary"]) // if(savedTemplateBlob) { // this.resume_workflow(); // } this.templateBlob = new TemplateBlob({}); this.labFlavors; // Map this.userTemplates; // List this.resourceBuilder; // ResourceBuilder this.templateBlob.public = false; } /** Finds the templateBlob object in the userTemplates list based on a given uuid */ getTemplateById(template_id) { for (let template of this.userTemplates) { if (template.id == template_id) { return template; } } return null; } resumeWorkflow() { todo() } /** Initializes the select_lab step */ async startWorkflow() { this.setPodDetailEventListeners(); const labs = await LibLaaSAPI.getLabs(); GUI.display_labs(labs); document.getElementById(this.sections[0]).scrollIntoView({behavior: 'smooth'}); } /** Adds the public network on start */ addDefaultNetwork() { const new_network = new NetworkBlob({}); new_network.name = "public"; new_network.public = true; this.addNetworkToPod(new_network); GUI.refreshNetworkStep(this.templateBlob.networks); } /** Takes an HTML element */ async onclickSelectLab(lab_card) { this.step = steps.SELECT_LAB; if (this.templateBlob.lab_name == null) { // Lab has not been selected yet this.templateBlob.lab_name = lab_card.id; lab_card.classList.add("selected_node"); await this.setLabDetails(this.templateBlob.lab_name); this.addDefaultNetwork(); } else { // Lab has been selected if(confirm('Unselecting a lab will reset all selected resources, are you sure?')) { location.reload(); } } } /** Calls the API to fetch flavors and images for a lab */ async setLabDetails(lab_name) { const flavorsList = await LibLaaSAPI.getLabFlavors(lab_name); this.labFlavors = new Map(); // Map this.labImages = new Map(); // Map for (const fblob of flavorsList) { fblob.images = await LibLaaSAPI.getImagesForFlavor(fblob.flavor_id); for (const iblob of fblob.images) { this.labImages.set(iblob.image_id, iblob) } this.labFlavors.set(fblob.flavor_id, fblob); } this.userTemplates = await LibLaaSAPI.getTemplatesForUser(); } /** Prepopulates fields and launches the modal */ onclickAddResource() { // Set step // Check prerequisites // Reset resourceBuilder // Generate template cards // Show modal this.step = steps.ADD_RESOURCES; if (this.templateBlob.lab_name == null) { alert("Please select a lab before adding resources."); this.goTo(steps.SELECT_LAB); return; } if (this.templateBlob.host_list.length >= 8) { alert("You may not add more than 8 hosts to a single pod.") return; } this.resourceBuilder = null; GUI.refreshAddHostModal(this.userTemplates); $("#resource_modal").modal('toggle'); } onclickSelectTemplate(template_id, card) { // Do nothing on reselect if (this.resourceBuilder && this.resourceBuilder.template_id == template_id) { return; } if (this.resourceBuilder) { GUI.unhighlightCard(document.querySelector('#template-cards .selected_node')); } this.resourceBuilder = new ResourceBuilder(this.getTemplateById(template_id)); GUI.highlightCard(card); GUI.refreshConfigSection(this.resourceBuilder, this.labFlavors); GUI.refreshInputSection(this.resourceBuilder, this.labFlavors); } onclickSelectNode(index) { this.resourceBuilder.tab = index; GUI.refreshInputSection(this.resourceBuilder, this.labFlavors); } onclickSelectImage(image_id, card) { const old_selection = document.querySelector("#image-cards .selected_node"); if (old_selection) { GUI.unhighlightCard(old_selection); } this.resourceBuilder.user_configs[this.resourceBuilder.tab].image = image_id; GUI.highlightCard(card.childNodes[1]); } /** Takes a string and returns a tuple containing the result and the error message (bool, string)*/ isValidHostname(hostname) { let result = true; let message = "success"; if (hostname == null || hostname == '') { result = false; message = 'Please enter a hostname'; } else if (hostname.length > 25) { result = false; message = 'Hostnames cannot exceed 25 characters'; } else if (!(hostname.match(/^[0-9a-z-]+$/i))) { result = false; message = 'Hostnames must only contain alphanumeric characters and dashes'; } else if ((hostname.charAt(0).match(/^[0-9-]+$/)) || (hostname.charAt(hostname.length - 1) == '-')) { result = false; message = 'Hostnames must start with a letter and end with a letter or digit.'; } return [result, message]; } /** Takes a hostname and a list of existing hosts and checks for duplicates in the existing hostlist*/ isUniqueHostname(hostname, existing_hosts) { for (const existing_host of existing_hosts) { if (hostname == existing_host.hostname) { return false; } } return true; } onclickSubmitHostConfig() { // Validate form fields // Create host config blobs // Apply networking // Create cards (refresh hostcard view) // Refresh networks view // Refresh connections view // Validate Configs for (const [index, host] of this.resourceBuilder.user_configs.entries()) { let result = this.isValidHostname(host.hostname); if (!result[0]) { this.resourceBuilder.tab = index; GUI.refreshConfigSection(this.resourceBuilder, this.labFlavors); GUI.refreshInputSection(this.resourceBuilder, this.labFlavors); GUI.showHostConfigErrorMessage(result[1]); return; } let uniqueHost = this.isUniqueHostname(host.hostname, this.templateBlob.host_list); if (!uniqueHost) { this.resourceBuilder.tab = index; GUI.refreshConfigSection(this.resourceBuilder, this.labFlavors); GUI.refreshInputSection(this.resourceBuilder, this.labFlavors); GUI.showHostConfigErrorMessage("Hostname '"+ host.hostname + "' already exists in Pod."); return; } if (index < this.resourceBuilder.user_configs.length - 1) { let uniqueConfigName = true; for (let i = index + 1; i < this.resourceBuilder.user_configs.length; i++) { if (host.hostname == this.resourceBuilder.user_configs[i].hostname) { uniqueConfigName = false; break; } } if (!uniqueConfigName) { this.resourceBuilder.tab = index; GUI.refreshConfigSection(this.resourceBuilder, this.labFlavors); GUI.refreshInputSection(this.resourceBuilder, this.labFlavors); GUI.showHostConfigErrorMessage("Hostname '"+ host.hostname + "' is a duplicate hostname."); return; } } // todo // let result2 = isValidCIFile(host.cifile[0]); } // Add host configs to TemplateBlob for (const [index, host] of this.resourceBuilder.user_configs.entries()) { const new_host = new HostConfigBlob(host); this.templateBlob.host_list.push(new_host); } // Add networks for (const n of this.resourceBuilder.networks) { if (!this.templateBlob.findNetwork(n.name)) { this.templateBlob.networks.push(n); } } // We are done GUI.refreshHostStep(this.templateBlob.host_list, this.labFlavors, this.labImages); GUI.refreshNetworkStep(this.templateBlob.networks); GUI.refreshConnectionStep(this.templateBlob.host_list); GUI.refreshPodSummaryHosts(this.templateBlob.host_list, this.labFlavors, this.labImages) $('#resource_modal').modal('hide') } /** * Takes a hostname, looks for the matching HostConfigBlob in the TemplateBlob, removes it from the list, and refreshes the appropriate views * @param {String} hostname */ onclickDeleteHost(hostname) { this.step = steps.ADD_RESOURCES; for (let existing_host of this.templateBlob.host_list) { if (hostname == existing_host.hostname) { this.removeHostFromTemplateBlob(existing_host); GUI.refreshHostStep(this.templateBlob.host_list, this.labFlavors, this.labImages); GUI.refreshNetworkStep(this.templateBlob.networks); GUI.refreshConnectionStep(this.templateBlob.host_list); GUI.refreshPodSummaryHosts(this.templateBlob.host_list, this.labFlavors, this.labImages); return; } } alert("didnt remove"); } /** onclick handler for the add_network_plus_card */ onclickAddNetwork() { // Set step // Prerequisite step checks // GUI stuff this.step = steps.ADD_NETWORKS; if (this.templateBlob.lab_name == null) { alert("Please select a lab before adding networks."); this.goTo(steps.SELECT_LAB); return; } if (document.querySelector('#new_network_card') != null) { alert("Please finish adding the current network before adding a new one."); return; } GUI.display_network_input(); } /** onclick handler for the adding_network_confirm button */ onclickConfirmNetwork() { this.step = steps.ADD_NETWORKS; // Add the network // call the GUI to make the card (refresh the whole view to make it easier) const new_network = new NetworkBlob({}); new_network.name = document.getElementById('network_input').value; new_network.public = document.getElementById('network-public-input').checked; const error_message = this.addNetworkToPod(new_network); if (error_message == null) { GUI.refreshNetworkStep(this.templateBlob.networks); GUI.refreshConnectionStep(this.templateBlob.host_list); } else { GUI.display_add_network_error(error_message); } } /** Takes a NetworkBlob and tries to add to the TemplateBlob. * Fails if input validation fails. * Returns error message or null. */ addNetworkToPod(networkBlob) { if (networkBlob.name == '' || networkBlob.name == null) { return "Network name cannot be empty."; } if (networkBlob.name.length > 25) { return 'Network names cannot exceed 25 characters'; } if (!(networkBlob.name.match(/^[0-9a-z-]+$/i))) { return 'Network names must only contain alphanumeric characters and dashes'; } if ((networkBlob.name.charAt(0).match(/^[0-9-]+$/)) || (networkBlob.name.charAt(networkBlob.name.length - 1) == '-')) { return 'Network names must start with a letter and end with a letter or digit.'; } for (let existing_network of this.templateBlob.networks) { if (networkBlob.name == existing_network.name) { return 'Networks must have unique names'; } } this.templateBlob.networks.push(networkBlob); return null; } /** Iterates through the templateBlob looking for the correct network to delete * Takes a network name as a parameter. */ onclickDeleteNetwork(network_name) { this.step = steps.ADD_NETWORKS; for (let existing_network of this.templateBlob.networks) { if (network_name == existing_network.name) { this.removeNetworkFromTemplateBlob(existing_network); this.removeConnectionsOnNetwork(existing_network.name) GUI.refreshNetworkStep(this.templateBlob.networks); GUI.refreshConnectionStep(this.templateBlob.host_list); return; } } alert("didnt remove"); } /** Rebuilds the list without the chosen template */ removeNetworkFromTemplateBlob(network_to_remove) { this.templateBlob.networks = this.templateBlob.networks.filter(network => network !== network_to_remove); } removeConnectionsOnNetwork(network_name) { for (const host of this.templateBlob.host_list) { for (const bg of host.bondgroups) { bg.connections = bg.connections.filter((connection) => connection.connects_to != network_name) } } } /** * Rebuilds the hostlist without the chosen host * Also removes all connections from this host's interfaces * @param {HostConfigBlob} hostBlob */ removeHostFromTemplateBlob(hostBlob) { this.templateBlob.host_list = this.templateBlob.host_list.filter(host => host !== hostBlob); } onclickConfigureConnection(hostname) { this.step = steps.CONFIGURE_CONNECTIONS; const host = this.templateBlob.findHost(hostname); if (!host) { alert("host not found error"); } this.connectionTemp = new ConnectionTemp(host, this.templateBlob.networks, this.labFlavors.get(host.flavor).interfaces); GUI.refreshConnectionModal(this.connectionTemp); $("#connection_modal").modal('toggle'); } onclickSelectIfaceTab(tab_index) { this.connectionTemp.selected_index = tab_index; GUI.refreshConnectionModal(this.connectionTemp); } onclickSelectVlan(network_name, tagged, iface_name) { const x = this.connectionTemp.config.get(iface_name); if (x.get(network_name) === tagged) { x.set(network_name, null); } else { x.set(network_name, tagged); } GUI.refreshConnectionTable(this.connectionTemp); } onclickSubmitConnectionConfig() { this.connectionTemp.applyConfigs(); GUI.refreshConnectionStep(this.templateBlob.host_list); } /** Sets input validation event listeners and clears the value in case of caching*/ setPodDetailEventListeners() { const pod_name_input = document.getElementById("pod-name-input"); const pod_desc_input = document.getElementById("pod-desc-input"); const pod_public_input = document.getElementById("pod-public-input"); pod_name_input.value = ""; pod_desc_input.value = ""; pod_public_input.checked = false; pod_name_input.addEventListener('focusout', (event)=> { workflow.onFocusOutPodNameInput(pod_name_input); }); pod_name_input.addEventListener('focusin', (event)=> { this.step = steps.POD_DETAILS; GUI.unhighlightError(pod_name_input); GUI.hidePodDetailsError(); }); pod_desc_input.addEventListener('focusout', (event)=> { workflow.onFocusOutPodDescInput(pod_desc_input); }); pod_desc_input.addEventListener('focusin', (event)=> { this.step = steps.POD_DETAILS; GUI.unhighlightError(pod_desc_input); GUI.hidePodDetailsError(); }); pod_public_input.addEventListener('focusout', (event)=> { this.step = steps.POD_DETAILS; workflow.onFocusOutPodPublicInput(pod_public_input); }); } onFocusOutPodNameInput(element) { const pod_name = element.value; const validator = this.validatePodInput(pod_name, 53, "Pod name"); if (validator[0]) { this.templateBlob.pod_name = pod_name; GUI.refreshPodSummaryDetails(this.templateBlob.pod_name, this.templateBlob.pod_desc, this.templateBlob.public) } else { GUI.highlightError(element); GUI.showPodDetailsError(validator[1]); } } onFocusOutPodDescInput(element) { const pod_desc = element.value; const validator = this.validatePodInput(pod_desc, 255, "Pod description"); if (validator[0]) { this.templateBlob.pod_desc = pod_desc; GUI.refreshPodSummaryDetails(this.templateBlob.pod_name, this.templateBlob.pod_desc, this.templateBlob.public) } else { GUI.highlightError(element); GUI.showPodDetailsError(validator[1]); } } onFocusOutPodPublicInput(element) { this.templateBlob.public = element.checked; GUI.refreshPodSummaryDetails(this.templateBlob.pod_name, this.templateBlob.pod_desc, this.templateBlob.public) } /** Returns a tuple containing result and message (bool, String) */ validatePodInput(input, maxCharCount, form_name) { let result = true; let message = "valid" if (input === '') { message = form_name + ' cannot be empty.'; result = false; } else if (input.length > maxCharCount) { message = form_name + ' cannot exceed ' + maxCharCount + ' characters.'; result = false; } else if (!(input.match(/^[a-z0-9~@#$^*()_+=[\]{}|,.?': -!]+$/i))) { message = form_name + ' contains invalid characters.'; result = false; } return [result, message] } async onclickDiscardTemplate() { this.step = steps.POD_SUMMARY; if(confirm('Are you sure you wish to delete this Pod?')) { await LibLaaSAPI.deleteTemplate(this.templateBlob); location.reload(); } } simpleStepValidation() { let passed = true; let message = "done"; let step = steps.POD_SUMMARY; if (this.templateBlob.lab_name == null) { passed = false; message = "Please select a lab"; step = steps.SELECT_LAB; } else if (this.templateBlob.host_list.length < 1 || this.templateBlob.host_list.length > 8) { passed = false; message = "Pods must contain 1 to 8 hosts"; step = steps.ADD_RESOURCES; } else if (this.templateBlob.networks.length < 1) { passed = false; message = "Pods must contain at least one network."; step = steps.ADD_NETWORKS; } else if (this.templateBlob.pod_name == null || this.templateBlob.pod_desc == null) { passed = false; message = "Please add a valid pod name and description."; step = steps.POD_DETAILS; } return [passed, message, step]; } async onclickSubmitTemplate() { this.step = steps.POD_SUMMARY; const simpleValidation = this.simpleStepValidation(); if (!simpleValidation[0]) { alert(simpleValidation[1]) this.goTo(simpleValidation[2]); return; } // todo - make sure each host has at least one connection on any network. if (confirm("Are you sure you wish to create this pod?")) { let success = await LibLaaSAPI.makeTemplate(this.templateBlob); if (success) { window.location.href = "../../accounts/my/resources/"; } else { alert("Could not create template.") } } } } /** View class that displays cards and generates HTML * Functions as a namespace, does not hold state */ class GUI { /** Takes a list of LabBlobs and creates a card for each of them on the screen */ static display_labs(labs) { const card_deck = document.getElementById('lab_cards'); for (let lab of labs) { const new_col = document.createElement('div'); new_col.classList.add('col-xl-3','col-md-6','col-11'); let status; if (lab.status == 0) { status = "Up"; } else if (lab.status == 100) { status = "Down for Maintenance"; } else if (lab.status == 200) { status = "Down"; } else { status = "Unknown"; } new_col.innerHTML = `

` + lab.name + `

  • Name: ` + lab.name + `
  • Description: ` + lab.description + `
  • Location: ` + lab.location + `
  • Status: `+ status + `
` card_deck.appendChild(new_col); } } static highlightCard(card) { card.classList.add('selected_node'); } static unhighlightCard(card) { card.classList.remove('selected_node'); } /** Resets the host modal inner html * Takes a list of templateBlobs */ static refreshAddHostModal(template_list) { document.getElementById('add_resource_modal_body').innerHTML = `

Resource

` const template_cards = document.getElementById('template-cards'); for (let template of template_list) { template_cards.appendChild(this.makeTemplateCard(template)); } } /** Makes a card to be displayed in the add resource modal for a given templateBlob */ static makeTemplateCard(templateBlob) { const col = document.createElement('div'); col.classList.add('col-12', 'col-md-6', 'col-xl-3', 'my-3'); col.innerHTML= `

` + templateBlob.pod_name + `

` + templateBlob.pod_desc +`

` return col; } /** Takes a ResourceBuilder and generates form fields */ static refreshConfigSection(resourceBuilder, flavors) { // Create a tab for head host in the selected template const tablist = document.getElementById('add_resource_tablist'); // ul tablist.innerHTML = ""; for (const [index, host] of resourceBuilder.user_configs.entries()) { const li_interface = document.createElement('li'); li_interface.classList.add('nav-item'); const btn_interface = document.createElement('a'); btn_interface.classList.add('nav-link', 'interface-btn'); btn_interface.id = "select-node-" + index; btn_interface.setAttribute("onclick", "workflow.onclickSelectNode("+ index + ")"); btn_interface.setAttribute('href', "#"); btn_interface.setAttribute('role', 'tab'); btn_interface.setAttribute('data-toggle', 'tab'); btn_interface.innerText = flavors.get(host.flavor).name; if (index == resourceBuilder.tab) { btn_interface.classList.add('active'); } li_interface.appendChild(btn_interface); tablist.appendChild(li_interface); } } static refreshInputSection(resourceBuilder, flavor_map) { // config stuff const image_cards = document.getElementById('image-cards'); const hostname_input = document.getElementById('hostname-input'); const ci_textarea = document.getElementById('ci-textarea'); const tab_flavor_id = resourceBuilder.original_configs[resourceBuilder.tab].flavor; const tab_flavor = flavor_map.get(tab_flavor_id); const image_list = tab_flavor.images; image_cards.innerHTML = ""; for (let imageBlob of image_list) { const new_image_card = this.makeImageCard(imageBlob); new_image_card.setAttribute("onclick", "workflow.onclickSelectImage('" + imageBlob.image_id + "', this)"); if (resourceBuilder.user_configs[resourceBuilder.tab].image == imageBlob.image_id) { GUI.highlightCard(new_image_card.childNodes[1]); } image_cards.appendChild(new_image_card); } // Hostname input hostname_input.value = resourceBuilder.user_configs[resourceBuilder.tab].hostname; hostname_input.addEventListener('focusout', (event)=> { resourceBuilder.user_configs[resourceBuilder.tab].hostname = hostname_input.value; }); hostname_input.addEventListener('focusin', (event)=> { this.removeHostConfigErrorMessage(); }); // CI input let ci_value = resourceBuilder.user_configs[resourceBuilder.tab].cifile[0]; if (!ci_value) { ci_value = ""; } ci_textarea.value = ci_value; ci_textarea.addEventListener('focusout', (event)=> { resourceBuilder.user_configs[resourceBuilder.tab].cifile[0] = ci_textarea.value; }) this.removeHostConfigErrorMessage(); document.getElementById('resource_config_section').removeAttribute('hidden'); } static showHostConfigErrorMessage(message) { document.getElementById("hostname-input").classList.add("invalid_field"); document.getElementById('add-host-error-msg').innerText = message; } static removeHostConfigErrorMessage() { document.getElementById("hostname-input").classList.remove("invalid_field"); document.getElementById('add-host-error-msg').innerText = ""; } static makeImageCard(imageBlob) { const col = document.createElement('div'); col.classList.add('col-12', 'col-md-6', 'col-xl-3', 'my-3'); col.innerHTML = `
` + imageBlob.name +`
` return col; } static highlightError(element) { element.classList.add('invalid_field'); } static unhighlightError(element) { element.classList.remove("invalid_field"); } static showPodDetailsError(message) { document.getElementById('pod_details_error').innerText = message; } static hidePodDetailsError() { document.getElementById('pod_details_error').innerText = "" } /** * Refreshes the step and creates a card for each host in the hostlist * @param {List} hostlist */ static refreshHostStep(hostlist, flavors, images) { const host_cards = document.getElementById('host_cards'); host_cards.innerHTML = ""; for (const host of hostlist) { host_cards.appendChild(this.makeHostCard(host, flavors, images)); } let span_class = '' if (hostlist.length == 8) { span_class = 'text-primary' } else if (hostlist.length > 8) { span_class = 'text-danger' } const plus_card = document.createElement("div"); plus_card.classList.add("col-xl-3", "col-md-6", "col-12"); plus_card.id = "add_resource_plus_card"; plus_card.innerHTML = `
` + hostlist.length + `/ 8
` host_cards.appendChild(plus_card); } /** * Makes a host card element for a given host and returns a reference to the card * @param {HostConfigBlob} host */ static makeHostCard(host, flavors, images) { const new_card = document.createElement("div"); new_card.classList.add("col-xl-3", "col-md-6","col-12", "my-3"); new_card.innerHTML = `

` + flavors.get(host.flavor).name + `

  • Hostname: ` + host.hostname + `
  • Image: ` + images.get(host.image).name + `
`; return new_card; } /** Shows the input card for adding a new network */ // Don't forget to redisable static display_network_input() { // New empty card const network_plus_card = document.getElementById('add_network_plus_card'); const new_card = document.createElement('div'); new_card.classList.add("col-xl-3", "col-md-6","col-12"); new_card.innerHTML = `

`; network_plus_card.parentNode.insertBefore(new_card, network_plus_card); document.getElementById('network_input').addEventListener('focusin', e => { document.getElementById('adding_network_error').innerText = ''; }) } static hide_network_input() { document.getElementById('new_network_card').parentElement.remove(); document.getElementById('add_network_plus_card').hidden = false; } /** Redraws all the cards on the network step. * Takes a list of networks to display */ static refreshNetworkStep(network_list) { document.getElementById('network_card_deck').innerHTML = `
` const network_plus_card = document.getElementById('add_network_plus_card'); for (let network of network_list) { // NetworkBlobs let pub_str = ' (private)'; if (network.public) { pub_str = ' (public)' } const new_card = document.createElement('div'); new_card.classList.add("col-xl-3", "col-md-6","col-12", "my-3"); new_card.innerHTML = `

` + network.name + pub_str +`

`; network_plus_card.parentNode.insertBefore(new_card, network_plus_card); } } /** Displays an error message on the add network card */ static display_add_network_error(error_message) { document.getElementById("adding_network_error").innerHTML = error_message; } static refreshConnectionStep(host_list) { const connection_cards = document.getElementById('connection_cards'); connection_cards.innerHTML = ""; for (const host of host_list) { connection_cards.appendChild(this.makeConnectionCard(host)); } } /** Makes a blank connection card that does not contain interface details */ static makeConnectionCard(host) { const new_card = document.createElement('div'); new_card.classList.add("col-xl-3", "col-md-6","col-11", "my-3"); const card_div = document.createElement('div'); card_div.classList.add('card'); new_card.appendChild(card_div); const card_header = document.createElement('div'); card_header.classList.add('card-header', 'text-center', 'p-0'); card_header.innerHTML = `

` + host.hostname + `

` const card_body = document.createElement('div'); card_body.classList.add('card-body', 'card-body-scroll', 'p-0'); const bondgroup_list = document.createElement('ul'); bondgroup_list.classList.add('list-group', 'list-group-flush', 'h-100') card_body.appendChild(bondgroup_list) const card_footer = document.createElement('div'); card_footer.classList.add('card-footer'); card_footer.innerHTML = `` card_div.appendChild(card_header); card_div.appendChild(card_body); card_div.appendChild(card_footer); for (const bg of host.bondgroups) { const outer_block = document.createElement('li'); outer_block.classList.add('list-group-item'); outer_block.innerHTML = `
` + bg.ifaces[0].name + `
` const inner_block = document.createElement('ul'); inner_block.classList.add('connection-holder'); outer_block.appendChild(inner_block) for (const c of bg.connections) { const connection_li = document.createElement('li'); connection_li.innerText = c.connects_to + `: ` + c.tagged; inner_block.appendChild(connection_li); } bondgroup_list.appendChild(outer_block) } return new_card; } /** */ static refreshConnectionModal(connectionTemp) { this.refreshConnectionTabs(connectionTemp.iface_list, connectionTemp.selected_index); this.refreshConnectionTable(connectionTemp); } /** Displays a tab in the connections modal for each interface * Returns the name of the currently selected interface for use in the connections table */ static refreshConnectionTabs (iface_list, iface_index) { const tablist_ul = document.getElementById('configure-connections-tablist'); tablist_ul.innerHTML = ''; for (const [index, iface_name] of iface_list.entries()) { const li_interface = document.createElement('li'); li_interface.classList.add('nav-item'); const btn_interface = document.createElement('a'); btn_interface.classList.add('nav-link', 'interface-btn'); btn_interface.setAttribute("onclick", "workflow.onclickSelectIfaceTab(" + index +")"); btn_interface.setAttribute('href', "#"); btn_interface.setAttribute('role', 'tab'); btn_interface.setAttribute('data-toggle', 'tab'); btn_interface.innerText = iface_name.name; li_interface.appendChild(btn_interface); tablist_ul.appendChild(li_interface); if (index == iface_index) { btn_interface.classList.add('active'); } } } static refreshConnectionTable(connectionTemp) { const connections_table = document.getElementById('connections_widget'); connections_table.innerHTML =` Network Vlan `; const selected_iface_name = connectionTemp.iface_list[connectionTemp.selected_index].name; const iface_config = connectionTemp.config.get(selected_iface_name); for (const network of connectionTemp.networks) { const tagged = iface_config.get(network.name); const new_row = document.createElement('tr'); const td_network = document.createElement('td'); td_network.innerText = network.name; new_row.appendChild(td_network); new_row.appendChild(this.makeTagTd(true, network.name, tagged === true, selected_iface_name)); new_row.appendChild(this.makeTagTd(false, network.name, tagged === false, selected_iface_name)); connections_table.appendChild(new_row); } // If an untagged is selected, disable all buttons that are not the selected button if (document.querySelector(".vlan-radio.untagged.btn-success")) { const other_buttons = document.querySelectorAll(".vlan-radio.untagged:not(.btn-success"); for (const btn of other_buttons) { btn.setAttribute("disabled", "true") } } } static makeTagTd(tagged, network_name, isSelected, selected_iface_name) { let tagged_as_str = "untagged" if (tagged) { tagged_as_str = "tagged" } const td = document.createElement('td'); const btn = document.createElement('button'); btn.classList.add("btn", "w-100", "h-100", "vlan-radio", "border", tagged_as_str); btn.setAttribute("onclick" ,"workflow.onclickSelectVlan('"+ network_name + "'," + tagged + ", '" + selected_iface_name +"')"); if (isSelected) { btn.classList.add('btn-success'); } btn.innerText = tagged_as_str; td.appendChild(btn); return td; } static refreshPodSummaryDetails(pod_name, pod_desc, isPublic) { const list = document.getElementById('pod_summary_pod_details'); list.innerHTML = ''; const name_li = document.createElement('li'); name_li.innerText = 'Pod name: ' + pod_name; list.appendChild(name_li); const desc_li = document.createElement('li') desc_li.innerText = 'Description: ' + pod_desc; list.appendChild(desc_li); const public_li = document.createElement('li'); public_li.innerText = 'Public: ' + isPublic; list.appendChild(public_li); } static refreshPodSummaryHosts(host_list, flavors, images) { const list = document.getElementById('pod_summary_hosts'); list.innerHTML = ''; for (const host of host_list) { const new_li = document.createElement('li'); // new_li.innerText = hosts[i].hostname + ': ' + this.lab_flavor_from_uuid(hosts[i].flavor).name + ' (' + hosts[i].image + ')'; const details = `${host.hostname}: ${flavors.get(host.flavor).name}, ${images.get(host.image).name}` new_li.innerText = details; list.appendChild(new_li); } } static update_pod_summary() { // Takes a section (string) and updates the appropriate element's innertext if (section == 'pod_details') { const list = document.getElementById('pod_summary_pod_details'); list.innerHTML = ''; const name_li = document.createElement('li'); name_li.innerText = 'Pod name: ' + this.pod.pod_name; list.appendChild(name_li); const desc_li = document.createElement('li') desc_li.innerText = 'Description: ' + this.pod.pod_desc; list.appendChild(desc_li); const public_li = document.createElement('li'); public_li.innerText = 'Public: ' + this.pod.is_public; list.appendChild(public_li); } else if (section == 'hosts') { const list = document.getElementById('pod_summary_hosts'); list.innerHTML = ''; const hosts = this.pod.host_list; for (let i = 0; i < this.pod.host_list.length; i++) { const new_li = document.createElement('li'); new_li.innerText = hosts[i].hostname + ': ' + this.lab_flavor_from_uuid(hosts[i].flavor).name + ' (' + hosts[i].image + ')'; list.appendChild(new_li); } } else { console.log(section + ' is not a valid section.'); } } } /** Holds in-memory configurations for the add resource step */ class ResourceBuilder { constructor(templateBlob) { this.template_id = templateBlob.id; // UUID (String) this.networks = templateBlob.networks; this.original_configs = templateBlob.host_list; // List this.user_configs = []; // List this.tab = 0; // Currently selected tab index where configs will be saved to // Create deep copies of the hosts for (let host of this.original_configs) { const copied_host = new HostConfigBlob(host); this.user_configs.push(copied_host); } } } class ConnectionTemp { // keep track of user inputs, commits to host bondgroups after user clicks submit constructor(host, networks, iface_list) { this.host = host; // reference this.config = new Map(); // Map}> this.iface_list = iface_list; // List for (const i of iface_list) { this.config.set(i.name, new Map()) } // set initial mappings for (const ebg of host.bondgroups) { // if (ebg.ifaces[0].name) const iface_config = this.config.get(ebg.ifaces[0].name); for (const c of ebg.connections) { iface_config.set(c.connects_to, c.tagged) } } this.networks = networks; // List this.selected_index = 0; } /** Replaces the old configs in the hostconfigblob with the ones set in this.config */ applyConfigs() { this.host.bondgroups = []; for (const [key, value] of this.config) { if (value.size > 0) { const full_iface = workflow.labFlavors.get(this.host.flavor).interfaces.filter((iface) => iface.name == key)[0]; const new_bg = new BondgroupBlob({}); this.host.bondgroups.push(new_bg) new_bg.ifaces.push(full_iface); for (const [network, tagged] of value) { if (tagged != null) { new_bg.connections.push(new ConnectionBlob({"tagged": tagged, "connects_to": network})) } } } } } } function todo() { alert('todo'); }