Frontend and backend changes needed to implement http://ovirt.org/page/Networking_UX#Option_3 --- src/app/controllers/host_controller.rb | 38 ++- src/app/services/host_service.rb | 35 ++ src/app/views/host/edit_network.rhtml | 593 ++++++++++++++++++++++++++++---- src/public/stylesheets/components.css | 76 ++++ 4 files changed, 676 insertions(+), 66 deletions(-) diff --git a/src/app/controllers/host_controller.rb b/src/app/controllers/host_controller.rb index 20e9fca..5cb89a9 100644 --- a/src/app/controllers/host_controller.rb +++ b/src/app/controllers/host_controller.rb @@ -104,10 +104,46 @@ class HostController < ApplicationController render :layout => 'popup' end + def update_network + # adjust parameters as necessary + params[:nics] = {} if params[:nics].nil? + params[:bondings] = {} if params[:bondings].nil? + params[:nics].each { |id,n| + unless n[:ip_address].nil? + # FIXME make this able to be v4 or v6 address + n[:ip_addresses] = [IpV4Address.new({ :address => n[:ip_address] })] + n.delete(:ip_address) + end + } + params[:bondings].each { |id,b| + unless b[:ip_address].nil? + # FIXME make this able to be v4 or v6 address + b[:ip_addresses] = [IpV4Address.new({ :address => b[:ip_address] })] + b.delete(:ip_address) + end + + if b[:nic_ids].nil? || b[:nic_ids].size == 0 + b[:nics] = [] + else + b[:nics] = b[:nic_ids].collect { |n| Nic.find(n) } + b.delete(:nic_ids) + end + + b[:interface_name] = b[:name] if b[:interface_name].nil? && !b[:name].nil? + } + params[:bondings].delete('NBID') # delete new bonding row + + svc_update_network(params[:id], params[:nics], params[:bondings]) + render :json => { + :object => "host", + :success => true, + :alert => "Host network entities successfully updated." + } + end + def bondings_json svc_show(params[:id]) bondings = @host.bondings render :json => bondings.collect{ |x| {:id => x.id, :name => x.name} } end - end diff --git a/src/app/services/host_service.rb b/src/app/services/host_service.rb index 4ace9fb..3b59f0e 100644 --- a/src/app/services/host_service.rb +++ b/src/app/services/host_service.rb @@ -72,6 +72,41 @@ module HostService # [<tt>Privilege::MODIFY</tt>] on host's HardwarePool def svc_modify(id) lookup(id, Privilege::MODIFY) + @networks = Network.find(:all) + @bonding_types = BondingType.find(:all) + end + + # Update the network entities associated with the Host with +id+ + # === Instance variables + # [<tt>@host</tt>] stores the Host with +id+ + # === Required permissions + # [<tt>Privilege::MODIFY</tt>] on host's HardwarePool + def svc_update_network(id, nics, bondings) + lookup(id, Privilege::MODIFY) + + nics.each { |nic_id, nic| + dnic = @host.nics.all.find { |n| n.id == nic_id.to_i } + unless dnic.nil? + dnic.update_attributes!(nic) + end + } + + # delete any bondings associated w/ host but not in bondings array + @host.bondings.each { |b| + b.destroy unless bondings.include?(b.id.to_s) + } + + bondings.each { |bond_id, bond| + dbond = @host.bondings.all.find { |b| b.id == bond_id.to_i } + unless dbond.nil? + # update original bonding + dbond.update_attributes!(bond) + else + bond[:host_id] = id + # create new bonding + Bonding.create!(bond) + end + } end # Set the disabled state of the Host with +id+ to <tt>:enabled</tt> diff --git a/src/app/views/host/edit_network.rhtml b/src/app/views/host/edit_network.rhtml index 760f508..1154595 100644 --- a/src/app/views/host/edit_network.rhtml +++ b/src/app/views/host/edit_network.rhtml @@ -6,80 +6,543 @@ Select and edit nics and bonded interfaces on <%= @host.hostname %> <%- end -%> -<div id="select-host-nic" class="popup-content-selection"> -<%= select_with_label "NICs", "nic", "id", - @host.nics. - collect{ |nic| [nic.interface_name.to_s + " " + nic.mac, nic.id] }. - insert(0, "") %> -</div> -<div id="select-host-bonding" class="popup-content-selection"> -<%= select_with_label "Bonded Interfaces", "bonding", "id", [] %> -</div> +<form id="host_network_form" method="POST" + action="<%= url_for :action => 'update_network' %>" > + +<%= hidden_field_tag 'id', @host.id %> +<div id="host_network_table"> + <div class="host_network_table_row"> + + <div class="host_network_table_device_column"> + <b>Device</b> + </div> + <div class="host_network_table_network_column"> + <b>Network</b> + </div> + + <div style="clear:both;"></div> + </div> + + <% @host.nics.each { |nic| %> + <div class="host_network_table_row" id="host_network_table_nic_<%= nic.id %>"> + + <div class="host_network_table_device_column host_network_table_aligned_column"> + <%= nic.interface_name + " " + nic.mac %> + </div> + + <div class="host_network_table_network_column"> + <select name="nics[<%= nic.id %>][network_id]" + id="host_network_table_select_net_nic_<%= nic.id %>" + class="host_network_table_select_net"> + + <option value="">Select a network...</option> + <% @networks.find_all { |net| net.type.to_s == 'PhysicalNetwork' }.each { |net| %> + <option value="<%= net.id %>" + <% if net.id == nic.network_id %>selected<% end %> > + <%= net.name %> + </option> + <% } %> + </select> + + <div class="host_network_table_subcolumn"> + Ip Address: + <input type="text" name="nics[<%= nic.id %>][ip_address]" + value="<%= nic.ip_address if nic.ip_address %>"> + + </input> + </div> + </div> + + <div class="host_network_table_buttons_column + host_network_table_aligned_column"> + <div class="host_network_table_edit_column"> + <%= image_tag("icon_edit_11px.png") %> + </div> + </div> + + <div style="clear:both;"></div> + </div> + <% } %> + + <% @host.bondings.each { |bonding| %> + <div class="host_network_table_row" + id="host_network_table_bonding_<%= bonding.id %>" > + + <div class="host_network_table_device_column host_network_table_aligned_column"> + <div class="host_network_table_show_label"> + <%= bonding.interface_name + " - " + bonding.bonding_type.label %> + </div> + <div class="host_network_table_edit_label" style="display: none;"> + Name: <input type="text" name="bondings[<%= bonding.id %>][name]" + value="<%= bonding.interface_name %>"/> + </div> + + <div class="host_network_table_subcolumn"> + <% bonding.nics.each { |nic| %> + <div class="host_network_table_subcolumn_row"> + <%= nic.interface_name + " " + nic.mac %> + </div> + <% } %> + + <% if bonding.nics.size == 0 %> + <i>No NICs associated</i> + <% end %> + </div> + + <div class="host_network_table_subcolumn" style="display: none;"> + NICs: + <select multiple name="bondings[<%= bonding.id %>][nic_ids][]" + id="host_network_table_select_nics_bonding_<%= bonding.id %>" + class="host_network_table_select_nics" > + + <% @host.nics.each { |nic| %> + <option value="<%= nic.id %>" + <% if bonding.nics.all.find{ |n| n.id == nic.id } %>selected<%end%> > + <%= nic.interface_name %> + </option> + <% } %> + </select> + </div> + + </div> + + <div class="host_network_table_network_column"> + <select name="bondings[<%= bonding.id %>[vlan_id]" + id="host_network_table_select_net_bonding_<%= bonding.id %>" + class="host_network_table_select_net" > + + <option value="">Select a network...</option> + <% @networks.find_all { |net| net.type.to_s == 'Vlan' }.each { |net| %> + <option value="<%= net.id %>" + <% if net.id == bonding.vlan_id %>selected<% end %> > + <%= net.name %> + </option> + <% } %> + </select> + + <div class="host_network_table_subcolumn"> + Ip Address: + <input type="text" + name="bondings[<%= bonding.id %>][ip_address]" + value="<%= bonding.ip_address if bonding.ip_address %>" /> + </div> + + <div class="host_network_table_subcolumn" style="display: none;"> + Bonding Type: + <select name="bondings[<%= bonding.id %>][bonding_type_id]"> + <% @bonding_types.each { |bt| %> + <option value="<%= bt.id %>" + <% if bt.id == bonding.bonding_type_id %>selected<% end %> > + <%= bt.label %> + </option> + <% } %> + </select> + </div> + </div> + + <div class="host_network_table_buttons_column host_network_table_aligned_column"> + <div class="host_network_table_edit_column"> + <%= image_tag("icon_edit_11px.png") %> + </div> + <div class="host_network_table_delete_column"> + <%= image_tag("icon_delete_11px.png") %> + </div> + </div> + + <div style="clear:both;"></div> + </div> -<div style="clear: both;"></div> -<div id="selected_nic_bonding" class="selected_popup_content"></div> + <% } %> + + <%# create hidden row for new bondings, NBID will be replaced w/ New Bonding ID %> + <div class="host_network_table_row" + id="host_network_table_new_bondings" + style="display:none;"> + + <div class="host_network_table_device_column host_network_table_aligned_column"> + <div class="host_network_table_edit_label"> + Name: <input type="text" name="bondings[NBID][name]" value=""/> + </div> + + <div class="host_network_table_subcolumn"> + NICs: + <select multiple name="bondings[NBID][nic_ids][]" + id="host_network_table_select_nics_bonding_NBID" + class="host_network_table_select_nics"> + <% @host.nics.each { |nic| %> + <option value="<%= nic.id %>"><%= nic.interface_name %></option> + <% } %> + </select> + </div> + </div> + + <div class="host_network_table_network_column"> + <select name="bondings[NBID][vlan_id]" + id="host_network_table_select_net_bonding_NBID" + class="host_network_table_select_net"> + <option value="">Select a network...</option> + <% @networks.find_all { |net| net.type.to_s == "Vlan" }.each { |net| %> + <option value="<%= net.id %>" > + <%= net.name %> + </option> + <% } %> + </select> + + <div class="host_network_table_subcolumn" style="display: none;"> + Ip Address: + <input type="text" name="bondings[NBID][ip_address]"/> + </div> + + <div class="host_network_table_subcolumn"> + Bonding Type: + <select name="bondings[NBID][bonding_type_id]"> + <% @bonding_types.each { |bt| %> + <option value="<%= bt.id %>"><%= bt.label %></option> + <% } %> + </select> + </div> + </div> + + <div class="host_network_table_buttons_column host_network_table_aligned_column"> + <div class="host_network_table_edit_column"> + + </div> + <div class="host_network_table_delete_column"> + <%= image_tag("icon_delete_11px.png") %> + </div> + </div> + + <div style="clear:both;"></div> + </div> + + <div class="host_network_table_row" style="border-bottom: none; padding-top: 18px;" > + + <div class="host_network_table_buttons_column" id="host_network_table_add_bonding" > + <%= image_tag("icon_add_11px.png") %> New Bonded Interface + </div> + <div style="clear:both;"></div> + </div> + +</div> <div id="host_network_footer" class="popup-content-footer"> - <%= ok_footer %> + <%= popup_footer("$('#host_network_form').submit()", "OK") %> </div> +</form> <script type="text/javascript"> -function reset_nics_bonding_detail(){ - var data='Select NIC or Bonded Interface<br/>'; - - $("#selected_nic_bonding").html(data); - $("#host_network_footer").show(); -}; - -reset_nics_bonding_detail(); // run it once for inital content - -function reset_nics_select(){ - $("#nic_id option:first").attr("selected", true); -}; - -function reset_bonding_select(){ - // incase of new additions / deletions, repopulate select box - $.getJSON( - "<%= url_for :action => 'bondings_json', :id => @host.id %>", - {}, - function(j){ - var options = "<option value=''></option>" + - "<option value='New'>New</option>"; - for(var i = 0; i < j.length; i++){ - options += '<option value="' + j[i].id + '">' + j[i].name + - '</option>'; + // create list of networks + var networks = new Array(); + <% @networks.each { |rnet| %> + jnet = new Object; + jnet.id = <%= rnet.id.to_s %>; + jnet.static_ip = <%= rnet.boot_type.proto == 'static' %>; + jnet.boot_type = "<%= rnet.boot_type.proto %>"; + jnet.selected = false; + networks.push(jnet); + <% } %> + + // the last network selected upon clicking a network select box + var last_network_selected = null; + + // create list of devices... + var devices = new Array(); + + // ...add nics to it + <% @host.nics.each { |rnic| %> + jnic = new Object; + jnic.type = 'nic'; + jnic.id = <%= rnic.id.to_s %>; + jnic.ip_address = "<%= rnic.ip_address ? rnic.ip_address : "" %>"; + jnic.network_id = "<%= rnic.network_id %>"; + devices.push(jnic); + <% } %> + + // ...add bondings to it + <% @host.bondings.each { |rbond| %> + jbond = new Object; + jbond.type = 'bonding'; + jbond.id = <%= rbond.id.to_s %>; + jbond.ip_address = "<%= rbond.ip_address ? rbond.ip_address : "" %>"; + jbond.network_id = "<%= rbond.vlan_id %>"; + jbond.nics = []; + devices.push(jbond); + <% } %> + + // generate an id greater than any the host currently has, + var current_device_id = 1; + for(j = 0; j < devices.length; ++j){ + if(devices[j].id >= current_device_id){ + current_device_id = devices[j].id + 1; + } + } + + // find network in networks array matching id + function find_network_by_id(network_id){ + for(j = 0; j < networks.length; ++j){ + if(networks[j].id == network_id){ + return networks[j]; } - $("#bonding_id").html(options); - }); - - $("#bonding_id option:first").attr("selected", true); -}; - -reset_bonding_select(); // run it once for initial content - -$("#nic_id").change(function () { - reset_bonding_select(); - if($('#nic_id').val() != ""){ - $("#selected_nic_bonding").load("<%= url_for :controller => 'network', - :action => 'edit_nic'%>/" + $('#nic_id').val()); - $("#host_network_footer").hide(); - }else{ - reset_nics_bonding_detail(); + } + return null; + } + + // parses device type out of a div id + function device_type_from_div_id(div_id){ + div_id = div_id.replace("host_network_table_", ""); + delim = div_id.search("_"); + result = div_id.substr(0, delim); + return result; + } + + // parses device id out of a div id + function device_id_from_div_id(div_id){ + div_id = div_id.replace("host_network_table_", ""); + delim = div_id.search("_"); + result = div_id.substr(delim+1); + return result; } -}); - -$("#bonding_id").change(function () { - reset_nics_select(); - if($('#bonding_id').val() == "New"){ - $("#selected_nic_bonding").load("<%= url_for :controller => 'network', :action => 'new_bonding', :host_id => @host.id %>"); - $("#host_network_footer").hide(); - }else if($('#bonding_id').val() != ""){ - $("#selected_nic_bonding").load("<%= url_for :controller => 'network', :action => 'edit_bonding'%>/" + $('#bonding_id').val()); - $("#host_network_footer").hide(); - }else{ - reset_nics_bonding_detail(); + + // find a device from a div id, searches devices array + // for device type/id pulled from the div id + function find_device(div_id){ + device_type = device_type_from_div_id(div_id); + device_id = device_id_from_div_id(div_id); + + for(j = 0; j < devices.length; ++j){ + if(devices[j].type == device_type && + devices[j].id == device_id){ + return devices[j]; + } + } + return null; + } + + // delete device specified by the given div id + function delete_device(div_id){ + device_type = device_type_from_div_id(div_id); + device_id = device_id_from_div_id(div_id); + + index_to_delete = -1; + for(j = 0; j < devices.length; ++j){ + if(devices[j].type == device_type && + devices[j].id == device_id){ + index_to_delete = j; + break; + } + } + + if(index_to_delete != -1){ + devices.splice(index_to_delete, 1); + return true; + } + + return false; + } + + // store currently selected network on select box clicked + // parameter should be the select box changed event + function on_network_select_box_clicked(e){ + last_network_selected = find_network_by_id(e.target.value); } -}); + + // modify table upon network select box changing + // parameter should be the select box changed event + function on_network_select_box_changed(e){ + // find network selected + network = find_network_by_id(e.target.value) + + // parse device out of row and set corresponding network id + device = find_device($(e.target).parent().parent()[0].id); + if(device != null){ + device.network_id = e.target.value; + } + + // mark network as selected, last net as unselected, and add/remove + // from other select boxes if appropriate + if(network != null){ + network.selected = true; + $('.host_network_table_select_net:not(#host_network_table_select_net_'+ + device.type+'_'+device.id+') option[@value='+network.id+']').hide(); + } + if(last_network_selected != null){ + last_network_selected.selected = false; + $('.host_network_table_select_net ' + + ' option[@value='+last_network_selected.id+']').show(); + } + + // for static ip networks show editable ip address textbox + if(network != null && network.static_ip){ + div = $(e.target).next('.host_network_table_subcolumn'); + div.show(); + div.children("input").removeAttr("disabled"); + + // grab value of address field from device + address = device ? device.ip_address : ""; + div.children("input").val(address); + + // for non-static networks disable/hide ip address textbox + }else{ + div = $(e.target).next('.host_network_table_subcolumn'); + div.hide(); + div.children("input").attr("disabled", true); + div.children("input").val(network ? network.boot_type : "Select network"); + } + } + + // modify table upon nics select box changing + // parameter should be the select box changed event + function on_nics_select_box_changed(e){ + // parse device out of row + device = find_device($(e.target).parent().parent().parent()[0].id); + if(device != null){ + device.nics = []; + } + + // hide selected attributes in all other selectboxes + $(e.target).children('option:selected').each(function(i){ + $('.host_network_table_select_nics:not(#host_network_table_select_nics_'+ + device.type+'_'+device.id+') option[@value='+this.value+']').hide(); + device.nics.push(this.value); + }); + + $(e.target).children('option:not(:selected):not(:hidden)').each(function(e){ + $('.host_network_table_select_nics '+ + ' option[@value='+this.value+']').show(); + }); + } + + // modify a row upon clicking the edit button + // toggles the visibility of various columns + // parameter should be the button img clicked event + function on_edit_button_clicked(e){ + // network column input fields + div = $(e.target).parent().parent(). + prev().children('.host_network_table_subcolumn'); + + // bring ip address column in line w/ + // other columns if applicable (bit of a hack + // to fix problem if ip address field is shown + // when edit button is clicked to edit) + if(div.size() == 2){ + if($(div[1]).is(":visible")){ + $(div[0]).show(); + }else{ + $(div[0]).hide(); + } + } + + // toggle network column inputs visibility + div.toggle(); + + // toggle show/edit label divs + div = $(e.target).parent().parent(). + prev().prev(); + div.children('.host_network_table_edit_label').toggle(); + div.children('.host_network_table_show_label').toggle(); + + // toggle show/edit nics divs + div = $(e.target).parent().parent(). + prev().prev().children('.host_network_table_subcolumn'); + div.toggle(); + } + + // delete when upon clicking the delete button + // parameters should be the button img clicked event + function on_delete_button_clicked(e){ + // mark network/nics as available + device = find_device($(e.target).parent().parent().parent()[0].id); + $('.host_network_table_select_net ' + + ' option[@value='+device.network_id+']').show(); + for(j = 0; j < device.nics.length; ++j){ + $('.host_network_table_select_nics '+ + ' option[@value='+device.nics[j]+']').show(); + } + + // delete device given by row id + delete_device($(e.target).parent().parent().parent()[0].id); + + // delete row which button is on + $(e.target).parent().parent().parent().remove(); + } + + // add new row to table using template when add new bonding button is clicked + // parameters should be the button div clicked event + function on_add_bonding_button_clicked(e){ + // create new bonding device + jbond = new Object; + jbond.type = 'bonding'; + jbond.id = current_device_id++; + jbond.ip_address = ""; + jbond.network_id = ""; + jbond.nics = []; + devices.push(jbond); + + // find and copy the new bonding template, append it as a new row + div = $("#host_network_table_new_bondings") + content = '<div class="host_network_table_row"'+ + 'id="host_network_table_bonding_'+jbond.id+'">'+ + div.html().replace(/NBID/g, jbond.id)+ + '</div>'; + div.before(content); + // set div id ? + } + + // wire up host_network_form when document is ready + function on_document_ready(){ + var opts = { + target: '<%= url_for :action => 'update_network' %>', // target element to update + dataType: 'json', + success: function(response, status) { + ajax_validation(response, status) + if (response.success) { + jQuery(document).trigger('close.facebox'); + $("#hosts_grid").flexReload() + refresh_summary('hosts_selection', + '<%= url_for :controller => "host", + :action => "show" %>', + <%= @host.id %>) + } + } + }; + + // bind form using 'ajaxForm' + $('#host_network_form').ajaxForm(opts); + } + + // handle network select box clicked event + $(".host_network_table_select_net"). + livequery('click', on_network_select_box_clicked). + bind('click', on_network_select_box_clicked); + + // handle network select box changed event + $(".host_network_table_select_net"). + livequery('change', on_network_select_box_changed). + bind('change', on_network_select_box_changed). + trigger('change'); + + // handle nics select box changed event + $(".host_network_table_select_nics"). + livequery('change', on_nics_select_box_changed). + bind('change', on_nics_select_box_changed). + trigger('change'); + + // when edit button is clicked + $(".host_network_table_edit_column img"). + bind('click', on_edit_button_clicked); + + // when delete button is clicked + $(".host_network_table_delete_column img"). + livequery("click", on_delete_button_clicked). + bind('click', on_delete_button_clicked); + + // when add bonding button is click + $("#host_network_table_add_bonding"). + bind("click", on_add_bonding_button_clicked); + + // when the document is ready + $(on_document_ready); + </script> diff --git a/src/public/stylesheets/components.css b/src/public/stylesheets/components.css index 2cda65d..b973b3b 100644 --- a/src/public/stylesheets/components.css +++ b/src/public/stylesheets/components.css @@ -389,3 +389,79 @@ #vm_network_config_add:hover { cursor: pointer; } + + +/* Host > Edit Network Configuration */ + +#host_network_table { + padding-top: 20px; + padding-left: 20px; + padding-bottom: 20px; + width: 93%; +} + +.host_network_table_row { + padding-top: 5px; + padding-bottom: 5px; + border-bottom: 1px solid gray; +} + +.host_network_table_device_column { + float: left; + width: 37%; +} + +.host_network_table_network_column { + float: left; + width: 49%; +} + +.host_network_table_subcolumn { + margin-top: 4px; +} + +.host_network_table_subcolumn input { + margin-top: 4px; + text-align: right; + width: 140px; +} + +.host_network_table_buttons_column { + float: left; + width: 8%; +} + +.host_network_table_buttons_column img { + cursor: pointer; +} + +.host_network_table_select_net { + min-width: 230px; +} + +.host_network_table_select_nics { + min-width: 160px; +} + +.host_network_table_edit_column, +.host_network_table_delete_column { + padding-top: 7px; + padding-left: 5px; + float: left; +} + +.host_network_table_show_label { + padding-top: 7px; +} + +.host_network_table_edit_label input { + width: 110px; +} + +#host_network_table_add_bonding { + border: 1px solid black; + padding: 5px; + width: 150px; + cursor: pointer; +} +/* ***** */ -- 1.6.0.6