diff --git a/console/templates/console-base.html b/console/templates/console-base.html index 694da9b..75a49c9 100644 --- a/console/templates/console-base.html +++ b/console/templates/console-base.html @@ -53,7 +53,7 @@ </head> <body> -<nav class="navbar navbar-inverse navbar-static-top" role="navigation"> +<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> diff --git a/console/templates/console-vnc-full.html b/console/templates/console-vnc-full.html index 6e99b88..61f90d3 100755 --- a/console/templates/console-vnc-full.html +++ b/console/templates/console-vnc-full.html @@ -80,7 +80,7 @@ {% block content %} <div id="noVNC_fallback_error" class="noVNC_center"> <div> - <div>noVNC encountered an error:</div> + <div>{% trans 'noVNC encountered an error' %}:</div> <br> <div id="noVNC_fallback_errormsg"></div> </div> diff --git a/console/templates/console-vnc-lite.html b/console/templates/console-vnc-lite.html index 4dd7bfe..50e41e3 100755 --- a/console/templates/console-vnc-lite.html +++ b/console/templates/console-vnc-lite.html @@ -294,7 +294,7 @@ {% block content %} <div id="noVNC_status_bar"> <div id="noVNC_left_dummy_elem"></div> - <div id="noVNC_status">Loading</div> + <div id="noVNC_status">{% trans 'Loading' %}</div> <div id="noVNC_buttons"> <input type=button value="Send CtrlAltDel" id="sendCtrlAltDelButton" class="noVNC_shown"> <span id="noVNC_power_buttons" class="noVNC_hidden"> diff --git a/create/views.py b/create/views.py index d44ad26..f8b4bc1 100644 --- a/create/views.py +++ b/create/views.py @@ -43,7 +43,7 @@ def create_instance(request, compute_id): compute.type) instances = conn.get_instances() - videos = conn.get_video() + videos = conn.get_video_models() cache_modes = sorted(conn.get_cache_modes().items()) default_cache = INSTANCE_VOLUME_DEFAULT_CACHE listener_addr = QEMU_CONSOLE_LISTEN_ADDRESSES diff --git a/datasource/templates/user_data b/datasource/templates/user_data index 4a94483..f6a4a58 100644 --- a/datasource/templates/user_data +++ b/datasource/templates/user_data @@ -1,6 +1,6 @@ #cloud-config {% if instance_keys %} ssh_authorized_keys: -{% for key in instance_keys %} - {{ key }}{% endfor %} +{% for key in instance_keys %} - {{ key }} +{% endfor %} {% endif %} - diff --git a/instances/templates/instance.html b/instances/templates/instance.html index 99adbcf..75f1def 100644 --- a/instances/templates/instance.html +++ b/instances/templates/instance.html @@ -432,11 +432,7 @@ <small><input type="checkbox" class="js-custom__checkbox" /> {% trans "Custom value" %}</small> </div> </div> - {% ifequal status 5 %} - <button type="submit" class="btn btn-lg btn-success pull-right" name="resizevm_mem">{% trans "Resize" %}</button> - {% else %} - <button class="btn btn-lg btn-success pull-right disabled">{% trans "Resize" %}</button> - {% endifequal %} + <button type="submit" class="btn btn-lg btn-success pull-right" name="resizevm_mem">{% trans "Resize" %}</button> </form> {% else %} {% trans "You don't have permission for resizing instance" %} @@ -851,53 +847,138 @@ </p> <div class="col-xs-12 col-sm-12"> - <form method="post" role="form">{% csrf_token %} - {% for network in networks %} - {% if forloop.first %} - <p><strong>{% trans "Network Devices" %}</strong></p> - {% endif %} - <div class="panel panel-default"> - <div class="panel-heading"> - <label>eth{{ forloop.counter0 }}({{ network.target|default:"no target" }})</label> - <button class="btn btn-sm pull-right btn-danger" value="{{ network.mac }}" name="delete_network" title="{% trans "Delete Device" %}" onclick="return confirm('{% trans "Are you sure?" %}')">{% trans "Delete" %}</button> - </div> - <div class="panel-body"> - <div class="form-group form-inline"> - <label class="col-sm-2 col-sm-offset-1 control-label">{% trans "MAC" %} </label> - <input class="form-control" type="text" value="{{ network.mac }}" readonly/> - <label class="control-label"><em>to</em></label> - <input class="form-control" type="text" name="net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/> - </div> - <div class="form-group form-inline"> - <label class="col-sm-2 col-sm-offset-1 control-label">{% trans "NIC" %} </label> - <input class="form-control" type="text" value="{{ network.nic }}" readonly/> - <label class="control-label"><em>to</em></label> - <select class="form-control" name="net-source-{{ forloop.counter0 }}"> - {% for c_net in networks_host %} - <option value="net:{{ c_net }}" {% ifequal c_net network.nic %} selected {% endifequal %}>{% trans 'Network' %} {{ c_net }}</option> - {% endfor %} - {% for c_iface in interfaces_host %} - <option value="iface:{{ c_iface }}" {% ifequal c_iface network.nic %} selected {% endifequal %}>{% trans 'Interface' %} {{ c_iface }}</option> - {% endfor %} - </select> - </div> - <div class="form-group form-inline"> - <label class="col-sm-2 col-sm-offset-1">{% trans "Filter" %} </label> - <input class="form-control" type="text" value="{{ network.filterref }}" readonly/> - <label class="control-label"><em>to</em></label> - <select class="form-control" name="net-nwfilter-{{ forloop.counter0 }}"> - <option value="">{% trans "None" %}</option> - {% for c_filters in nwfilters_host %} - <option value="{{ c_filters }}" {% ifequal c_filters network.filterref %} selected {% endifequal %}>{{ c_filters }}</option> - {% endfor %} - </select> - </div> - <button class="btn btn-sm btn-primary btn-block" name="change_network" title="{% trans "Apply Network Changes" %}">{% trans "Apply" %}</button> - </div> - </div> - {% endfor %} - </form> + <p><strong>{% trans "Network Devices" %}</strong></p> + <table class="table table-hover"> + <thead> + <tr> + <th>{% trans 'Name' %}</th> + <th>{% trans 'MAC' %}</th> + <th>{% trans 'NIC' %}</th> + <th>{% trans 'Filter' %}</th> + <th>{% trans 'Actions' %}</th> + </tr> + </thead> + <tbody> + {% for network in networks %} + <tr> + <td class="col-sm-2"><label>eth{{ forloop.counter0 }}({{ network.target|default:"no target" }})</label></td> + <td><input class="form-control" type="text" value="{{ network.mac }}" readonly/></td> + <td><input class="form-control" type="text" value="{{ network.nic }}" readonly/></td> + <td><input class="form-control" type="text" value="{{ network.filterref }}" readonly/></td> + <td class="col-sm-2"> + <form class="form-horizontal" method="post" name="set_qos{{ forloop.counter0 }}" role="form">{% csrf_token %} + <button data-target="#editInstanceNetwork{{ forloop.counter0 }}" type="button" class="btn btn-sm btn-primary" + title="Edit NIC" data-toggle="modal"> + <span class="glyphicon glyphicon-edit" aria-hidden="true"></span> + </button> + + <div class="modal fade" id="editInstanceNetwork{{ forloop.counter0 }}" role="dialog" aria-labelledby="editInstanceNetworkLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title">{% trans "Edit Instance Network" %}</h4> + </div> + <div class="modal-body"> + <div class="form-group form-inline"> + <label class="col-sm-2 control-label">{% trans "MAC" %} </label> + <input class="form-control" type="text" value="{{ network.mac }}" readonly/> + <label class="control-label"><em>to</em></label> + <input class="form-control" type="text" name="net-mac-{{ forloop.counter0 }}" value="{{ network.mac }}"/> + </div> + <div class="form-group form-inline"> + <label class="col-sm-2 control-label">{% trans "NIC" %} </label> + <input class="form-control" type="text" value="{{ network.nic }}" readonly/> + <label class="control-label"><em>to</em></label> + <select class="form-control" name="net-source-{{ forloop.counter0 }}"> + {% for c_net in networks_host %} + <option value="net:{{ c_net }}" {% ifequal c_net network.nic %} selected {% endifequal %}>{% trans 'Network' %} {{ c_net }}</option> + {% endfor %} + {% for c_iface in interfaces_host %} + <option value="iface:{{ c_iface }}" {% ifequal c_iface network.nic %} selected {% endifequal %}>{% trans 'Interface' %} {{ c_iface }}</option> + {% endfor %} + </select> + </div> + <div class="form-group form-inline"> + <label class="col-sm-2 control-label">{% trans "Filter" %} </label> + <input class="form-control" type="text" value="{{ network.filterref }}" readonly/> + <label class="control-label"><em>to</em></label> + <select class="form-control" name="net-nwfilter-{{ forloop.counter0 }}"> + <option value="">{% trans "None" %}</option> + {% for c_filters in nwfilters_host %} + <option value="{{ c_filters }}" {% ifequal c_filters network.filterref %} selected {% endifequal %}>{{ c_filters }}</option> + {% endfor %} + </select> + </div> + <button class="btn btn-sm btn-primary btn-block" name="change_network" title="{% trans "Apply Network Changes" %}">{% trans "Apply" %}</button> + </div> + </div> + </div> + </div> + + <button class="btn btn-sm btn-danger" value="{{ network.mac }}" name="delete_network" title="{% trans "Delete Device" %}" + onclick="return confirm('{% trans "Are you sure?" %}')"> + <i class="glyphicon glyphicon-trash"></i> + </button> + + {% include 'add_network_qos.html' with id=forloop.counter0 %} + </form> + </td> + </tr> + {% endfor %} + </tbody> + </table> </div> + + {% if qos %} + <div class="col-xs-10 col-sm-10"> + <p><strong>{% trans "Qos Configuration" %}</strong></p> + </div> + <div class="col-xs-12 col-sm-12"> + <table class="table table-hover"> + <thead> + <tr> + <th>{% trans "Direction" %}</th> + <th>{% trans "Average" %}</th> + <th>{% trans "Peak" %}</th> + <th>{% trans "Burst" %}</th> + <th>{% trans "Actions" %}</th> + </tr> + </thead> + <tbody> + {% for q, attrs in qos.items %} + {% for att in attrs %} + <form method="post" role="form">{% csrf_token %} + <tr> + <td><label class="control-label">{{ q }} {{ att.direction | capfirst }}</label></td> + <td><input id="qos_average" class="form-control" name="qos_average" + value="{{ att.average|default:'' }}"/></td> + <td><input id="qos_peak" class="form-control" name="qos_peak" + value="{{ att.peak|default:'' }}"/></td> + <td><input id="qos_burst" class="form-control" name="qos_burst" + value="{{ att.burst|default:'' }}"/></td> + <td class="col-sm-2"> + <input name="qos_direction" value="{{ att.direction }}" hidden/> + <input name="net-mac" value="{{ q }}" hidden/> + <button type="submit" class="btn btn-sm btn-primary" + name="set_qos" data-toggle="modal" + title="Edit Qos" onclick="return confirm('{% trans "Are you sure?" %}')"> + <i class="glyphicon glyphicon-save"></i> + </button> + <button type="submit" class="btn btn-sm btn-danger" + name="unset_qos" + title="Delete Qos" onclick="return confirm('{% trans "Are you sure?" %}')"> + <i class="glyphicon glyphicon-trash"></i> + </button> + </td> + </tr> + </form> + {% endfor %} + {% endfor %} + </tbody> + </table> + </div> + {% endif %} <div class="clearfix"></div> </div> <div role="tabpanel" class="tab-pane tab-pane-bordered" id="migrate"> @@ -1209,6 +1290,8 @@ <div class="clearfix"></div> </div> <div role="tabpanel" class="tab-pane tab-pane-bordered" id="options"> + <div class="well"> + <p>{% trans "To set instance template name description, shutdown the instance." %}</p> <form class="form-horizontal" action="" method="post" role="form">{% csrf_token %} <div class="form-group"> <label class="col-sm-3 control-label">{% trans "Title" %}</label> @@ -1225,17 +1308,52 @@ <div class="form-group"> <label class="col-sm-3 control-label">{% trans "Is template" %}</label> <div class="col-sm-6"> - <input type="checkbox" name="is_template" value="True" id="is_template" {% if instance.is_template %}checked{% endif %} {% if not request.user.is_superuser %}disabled{% endif %}> + <input type="checkbox" + name="is_template" + value="True" + id="is_template" + {% if instance.is_template %}checked{% endif %} + {% if not request.user.is_superuser and not request.user.is_staff %}disabled{% endif %}> + </div> + </div> + <div class="form-group "> + <div class="col-sm-offset-3 col-sm-6"> + {% ifequal status 5 %} + <button type="submit" class="btn btn-block btn-success" name="change_options">{% trans "Change" %}</button> + {% else %} + <button class="btn btn-block btn-success disabled" name="change_options">{% trans "Change" %}</button> + {% endifequal %} + </div> + </div> + </form> + </div> + <div class="well"> + <p>{% trans "To set instance video model, shutdown the instance." %}</p> + <form class="form-horizontal" method="post" role="form">{% csrf_token %} + <div class="form-group"> + <label for="video_model_select" class="col-sm-3 control-label">{% trans "Primary Video Model" %}</label> + <div class="col-sm-6"> + <div class="input-group"> + <select id="video_model_select" class="form-control" name="video_model"> + <option value="" style="font-weight: bold">{% trans "please choose" %}</option> + {% for vmodel in videos_host %} + <option value="{{ vmodel }}">{{ vmodel }}</option> + {% endfor %} + </select> + <span class="input-group-btn"> + {% ifequal status 5 %} + <button type="submit" class="btn btn-success" name="set_video_model">{% trans "Set" %}</button> + {% else %} + <button class="btn btn-success disabled" name="set_video_model">{% trans "Set" %}</button> + {% endifequal %} + </span> + </div> </div> </div> - {% ifequal status 5 %} - <button type="submit" class="btn btn-lg btn-success pull-right" name="change_options">{% trans "Change" %}</button> - {% else %} - <button class="btn btn-lg btn-success pull-right disabled" name="change_options">{% trans "Change" %}</button> - {% endifequal %} </form> <div class="clearfix"></div> </div> + </div> {% endif %} </div> </div> @@ -1516,6 +1634,13 @@ $("#console_select_listen_address option[value='" + console_listen_address + "']").prop('selected', true); } }); + $(document).ready(function () { + // get video model or fall back to default + let video_model = "{{ video_model }}" + if (video_model != '') { + $("#video_model_select option[value='" + video_model + "']").prop('selected', true); + } + }); $(document).ready(function () { // set vdi url $.get("{% url 'vdi_url' vname %}", function(data) { diff --git a/instances/views.py b/instances/views.py index 474bfd2..51246ce 100644 --- a/instances/views.py +++ b/instances/views.py @@ -272,6 +272,7 @@ def instance(request, compute_id, vname): title = conn.get_title() description = conn.get_description() networks = conn.get_net_device() + qos = conn.get_all_qos() disks = conn.get_disk_devices() media = conn.get_media_devices() if len(media) != 0: @@ -290,6 +291,7 @@ def instance(request, compute_id, vname): console_port = conn.get_console_port() console_keymap = conn.get_console_keymap() console_listen_address = conn.get_console_listen_addr() + video_model = conn.get_video_model() snapshots = sorted(conn.get_snapshot(), reverse=True, key=lambda k: k['date']) inst_xml = conn._XMLDesc(VIR_DOMAIN_XML_SECURE) has_managed_save_image = conn.get_managed_save_image() @@ -327,6 +329,7 @@ def instance(request, compute_id, vname): vcpu_host = len(vcpu_range) memory_host = conn.get_max_memory() bus_host = conn.get_disk_bus_types() + videos_host = conn.get_video_models() networks_host = sorted(conn.get_networks()) interfaces_host = sorted(conn.get_ifaces()) nwfilters_host = conn.get_nwfilters() @@ -480,10 +483,11 @@ def instance(request, compute_id, vname): conn.resize_cpu(cur_vcpu, vcpu) msg = _("Resize CPU") addlogmsg(request.user.username, instance.name, msg) - return HttpResponseRedirect(request.get_full_path() + '#resize') + return HttpResponseRedirect(request.get_full_path() + '#resize') - if 'resizevm_mem' in request.POST and ( - request.user.is_superuser or request.user.is_staff or userinstance.is_change): + if 'resizevm_mem' in request.POST and (request.user.is_superuser or + request.user.is_staff or + userinstance.is_change): new_memory = request.POST.get('memory', '') new_memory_custom = request.POST.get('memory_custom', '') if new_memory_custom: @@ -502,7 +506,7 @@ def instance(request, compute_id, vname): conn.resize_mem(cur_memory, memory) msg = _("Resize Memory") addlogmsg(request.user.username, instance.name, msg) - return HttpResponseRedirect(request.get_full_path() + '#resize') + return HttpResponseRedirect(request.get_full_path() + '#resize') if 'resizevm_disk' in request.POST and ( request.user.is_superuser or request.user.is_staff or userinstance.is_change): @@ -522,7 +526,7 @@ def instance(request, compute_id, vname): conn.resize_disk(disks_new) msg = _("Resize") addlogmsg(request.user.username, instance.name, msg) - return HttpResponseRedirect(request.get_full_path() + '#resize') + return HttpResponseRedirect(request.get_full_path() + '#resize') if 'add_new_vol' in request.POST and allow_admin_or_not_template: conn_create = wvmCreate(compute.hostname, @@ -699,7 +703,8 @@ def instance(request, compute_id, vname): msg = _("Set boot order") if not conn.get_status() == 5: - messages.success(request, _("Boot menu changes applied. But it will be activated after shutdown")) + messages.success(request, _("Boot menu changes applied. " + + "But it will be activated after shutdown")) else: messages.success(request, _("Boot order changed successfully.")) addlogmsg(request.user.username, instance.name, msg) @@ -727,7 +732,8 @@ def instance(request, compute_id, vname): error_messages.append(msg) if not error_messages: if not conn.set_console_passwd(passwd): - msg = _("Error setting console password. You should check that your instance have an graphic device.") + msg = _("Error setting console password. " + + "You should check that your instance have an graphic device.") error_messages.append(msg) else: msg = _("Set VNC password") @@ -760,6 +766,13 @@ def instance(request, compute_id, vname): return HttpResponseRedirect(request.get_full_path() + '#vncsettings') if request.user.is_superuser: + if 'set_video_model' in request.POST: + video_model = request.POST.get('video_model', 'vga') + conn.set_video_model(video_model) + msg = _("Set Video Model") + addlogmsg(request.user.username, instance.name, msg) + return HttpResponseRedirect(request.get_full_path() + '#options') + if 'migrate' in request.POST: compute_id = request.POST.get('compute_id', '') live = request.POST.get('live_migrate', False) @@ -808,6 +821,40 @@ def instance(request, compute_id, vname): addlogmsg(request.user.username, instance.name, msg) return HttpResponseRedirect(request.get_full_path() + '#network') + if 'set_qos' in request.POST: + qos_dir = request.POST.get('qos_direction', '') + average = request.POST.get('qos_average') or 0 + peak = request.POST.get('qos_peak') or 0 + burst = request.POST.get('qos_burst') or 0 + keys = request.POST.keys() + mac_key = [key for key in keys if 'mac' in key] + if mac_key: mac = request.POST.get(mac_key[0]) + + try: + conn.set_qos(mac, qos_dir, average, peak, burst) + if conn.get_status() == 5: + messages.success(request, "{} Qos is set".format(qos_dir.capitalize())) + else: + messages.success(request, + "{} Qos is set. Network XML is changed.".format(qos_dir.capitalize()) + + "Stop and start network to activate new config") + + except libvirtError as le: + messages.error(request, le.message) + return HttpResponseRedirect(request.get_full_path() + '#network') + if 'unset_qos' in request.POST: + qos_dir = request.POST.get('qos_direction', '') + mac = request.POST.get('net-mac') + conn.unset_qos(mac, qos_dir) + + if conn.get_status() == 5: + messages.success(request, "{} Qos is deleted".format(qos_dir.capitalize())) + else: + messages.success(request, + "{} Qos is deleted. Network XML is changed. ".format(qos_dir.capitalize()) + + "Stop and start network to activate new config.") + return HttpResponseRedirect(request.get_full_path() + '#network') + if 'add_owner' in request.POST: user_id = int(request.POST.get('user_id', '')) @@ -1014,7 +1061,7 @@ def get_host_instances(request, comp): conn.close() else: - raise libvirtError("Problem occured with {} - {}".format(comp.name, status)) + raise libvirtError("Problem occurred with {} - {}".format(comp.name, status)) return all_host_vms diff --git a/networks/templates/add_inbound_qos.html b/networks/templates/add_network_qos.html similarity index 57% rename from networks/templates/add_inbound_qos.html rename to networks/templates/add_network_qos.html index 53b29e5..8ff5043 100644 --- a/networks/templates/add_inbound_qos.html +++ b/networks/templates/add_network_qos.html @@ -1,11 +1,11 @@ {% load i18n %} {% if request.user.is_superuser %} - <a href="#AddInboundQos" type="button" class="btn btn-success pull-right" data-toggle="modal" title="add inbound qos"> - <span class="glyphicon glyphicon-arrow-down" aria-hidden="true"></span> + <a href="#AddQos{{ id }}" type="button" class="btn btn-sm btn-success pull-right" data-toggle="modal" title="add qos"> + <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> </a> <!-- Modal pool --> - <div class="modal fade" id="AddInboundQos" tabindex="-1" role="dialog" aria-labelledby="AddInboundQosLabel" + <div class="modal fade" id="AddQos{{ id }}" tabindex="-1" role="dialog" aria-labelledby="AddQosLabel" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> @@ -13,36 +13,43 @@ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title">{% trans "Add Inbound Qos for Network" %}</h4> </div> - <div class="modal-body"> - <form class="form-horizontal" method="post" name="set_qos" role="form">{% csrf_token %} + <div class="form-group"> + <label class="col-sm-4 control-label">{% trans "Direction" %}:</label> + <div class="col-sm-6"> + <select class="form-control" name="qos_direction"> + <option value="inbound">{% trans 'Inbound' %}</option> + <option value="outbound">{% trans 'Outbound' %}</option> + </select> + </div> + </div> <div class="form-group"> <label class="col-sm-4 control-label">{% trans "Average" %}:</label> <div class="col-sm-6"> - <input class="form-control" name="qos_inbound_average" required pattern="[0-9]+"/> + <input class="form-control" name="qos_average" placeholder="kilobytes" + required pattern="[0-9]+"/> </div> </div> <div class="form-group"> <label class="col-sm-4 control-label">{% trans "Peak" %}:</label> <div class="col-sm-6"> - <input class="form-control" name="qos_inbound_peak" - required pattern="[0-9]+"/> + <input class="form-control" name="qos_peak" placeholder="kilobytes" + pattern="[0-9]+"/> </div> </div> <div class="form-group"> <label class="col-sm-4 control-label">{% trans "Burst" %}:</label> <div class="col-sm-6"> - <input class="form-control" name="qos_inbound_burst" required pattern="[0-9]+"/> + <input class="form-control" name="qos_burst" placeholder="kilobytes" + pattern="[0-9]+"/> </div> </div> - <input name="qos_direction" value="inbound" hidden/> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">{% trans 'Close' %}</button> <button type="submit" class="btn btn-primary" name="set_qos">{% trans 'Save' %}</button> </div> - </form> </div> </div> <!-- /.modal-dialog --> </div> <!-- /.modal --> diff --git a/networks/templates/add_outbound_qos.html b/networks/templates/add_outbound_qos.html deleted file mode 100644 index 0e11640..0000000 --- a/networks/templates/add_outbound_qos.html +++ /dev/null @@ -1,49 +0,0 @@ -{% load i18n %} -{% if request.user.is_superuser %} - <a href="#AddOutboundQos" type="button" class="btn btn-success pull-right" data-toggle="modal" title="add outbound qos"> - <span class="glyphicon glyphicon-arrow-up" aria-hidden="true"></span> - </a> - - <!-- Modal pool --> - <div class="modal fade" id="AddOutboundQos" tabindex="-1" role="dialog" aria-labelledby="AddOutboundQosLabel" - aria-hidden="true"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h4 class="modal-title">{% trans "Add Outbound Qos for Network" %}</h4> - </div> - - <div class="modal-body"> - <form class="form-horizontal" method="post" name="set_qos" role="form">{% csrf_token %} - <div class="form-group"> - <label class="col-sm-4 control-label">{% trans "Average" %}:</label> - <div class="col-sm-6"> - <input class="form-control" name="qos_outbound_average" required pattern="[0-9]+"/> - </div> - </div> - <div class="form-group"> - <label class="col-sm-4 control-label">{% trans "Peak" %}:</label> - <div class="col-sm-6"> - <input class="form-control" name="qos_outbound_peak" - required pattern="[0-9]+"/> - </div> - </div> - <div class="form-group"> - <label class="col-sm-4 control-label">{% trans "Burst" %}:</label> - <div class="col-sm-6"> - <input class="form-control" name="qos_outbound_burst" required pattern="[0-9]+"/> - </div> - </div> - <input name="qos_direction" value="outbound" hidden/> - </div> - - <div class="modal-footer"> - <button type="button" class="btn btn-default" data-dismiss="modal">{% trans 'Close' %}</button> - <button type="submit" class="btn btn-primary" name="set_qos">{% trans 'Save' %}</button> - </div> - </form> - </div> - </div> <!-- /.modal-dialog --> - </div> <!-- /.modal --> -{% endif %} \ No newline at end of file diff --git a/networks/templates/network.html b/networks/templates/network.html index 0f1c832..45aba51 100644 --- a/networks/templates/network.html +++ b/networks/templates/network.html @@ -339,59 +339,61 @@ </div> {% endif %} - {% ifequal state 0 %} - {% include 'add_outbound_qos.html' %} - {% include 'add_inbound_qos.html' %} - {% endifequal %} + {% if net_forward.0 == 'route' or net_forward.0 == 'nat' or net_forward.0 == 'isolated' %} + {% if state == 0 and qos.items|length != 2%} + <form class="form-horizontal" method="post" name="set_qos" role="form">{% csrf_token %} + {% include 'add_network_qos.html' %} + </form> + {% endif %} - <div class="row"> - <h3 class="page-header">{% trans "Qos Configuration" %} - </h3> - </div> - - <div class="row"> - <div class="col-sm-12"> - <table class="table table-hover"> - <thead> - <tr> - <th style="text-align: center">{% trans "Direction" %}</th> - <th style="text-align: center">{% trans "Average" %}</th> - <th style="text-align: center">{% trans "Peak" %}</th> - <th style="text-align: center">{% trans "Burst" %}</th> - <th style="text-align: center">{% trans "Actions" %}</th> - </tr> - </thead> - <tbody> - {% for q, att in qos.items %} - <form method="post" role="form">{% csrf_token %} - <tr> - <td><label class="control-label">{{ q | capfirst }}</label></td> - <td><input id="qos_{{ q }}_av" class="form-control" name="qos_{{ q }}_average" - value="{{ att.average }}"/></td> - <td><input id="qos_{{ q }}_peak" class="form-control" name="qos_{{ q }}_peak" - value="{{ att.peak }}"/></td> - <td><input id="qos_{{ q }}_burst" class="form-control" name="qos_{{ q }}_burst" - value="{{ att.burst }}"/></td> - <td> - <input name="qos_direction" value="{{ q }}" hidden/> - <button type="submit" class="btn btn-sm btn-primary" - name="set_qos" - title="Edit Qos" onclick="return confirm('{% trans "Are you sure?" %}')"> - <i class="glyphicon glyphicon-save"></i> - </button> - <button type="submit" class="btn btn-sm btn-danger" - name="unset_qos" - title="Delete Qos" onclick="return confirm('{% trans "Are you sure?" %}')"> - <i class="glyphicon glyphicon-trash"></i> - </button> - </td> - </tr> - </form> - {% endfor %} - </tbody> - </table> + <div class="row"> + <h3 class="page-header">{% trans "Qos Configuration" %}</h3> </div> - </div> + + <div class="row"> + <div class="col-sm-12"> + <table class="table table-hover"> + <thead> + <tr> + <th style="text-align: center">{% trans "Direction" %}</th> + <th style="text-align: center">{% trans "Average" %}</th> + <th style="text-align: center">{% trans "Peak" %}</th> + <th style="text-align: center">{% trans "Burst" %}</th> + <th style="text-align: center">{% trans "Actions" %}</th> + </tr> + </thead> + <tbody> + {% for q, att in qos.items %} + <form method="post" role="form">{% csrf_token %} + <tr> + <td><label class="control-label">{{ q | capfirst }}</label></td> + <td><input id="qos_average" class="form-control" name="qos_average" + value="{{ att.average|default:'' }}"/></td> + <td><input id="qos_peak" class="form-control" name="qos_peak" + value="{{ att.peak|default:'' }}"/></td> + <td><input id="qos_burst" class="form-control" name="qos_burst" + value="{{ att.burst|default:'' }}"/></td> + <td> + <input name="qos_direction" value="{{ q }}" hidden/> + <button type="submit" class="btn btn-sm btn-primary" + name="set_qos" data-toggle="modal" + title="Edit Qos" onclick="return confirm('{% trans "Are you sure?" %}')"> + <i class="glyphicon glyphicon-save"></i> + </button> + <button type="submit" class="btn btn-sm btn-danger" + name="unset_qos" + title="Delete Qos" onclick="return confirm('{% trans "Are you sure?" %}')"> + <i class="glyphicon glyphicon-trash"></i> + </button> + </td> + </tr> + </form> + {% endfor %} + </tbody> + </table> + </div> + </div> + {% endif %} {% endblock %} {% block script %} <script> diff --git a/networks/views.py b/networks/views.py index 38bbd09..15f10bc 100644 --- a/networks/views.py +++ b/networks/views.py @@ -195,19 +195,21 @@ def network(request, compute_id, pool): else: messages.success(request, _("Network XML is changed.")) return HttpResponseRedirect(request.get_full_path()) - if 'set_qos' in request.POST: qos_dir = request.POST.get('qos_direction', '') - average = request.POST.get('qos_{}_average'.format(qos_dir), '') - peak = request.POST.get('qos_{}_peak'.format(qos_dir), '') - burst = request.POST.get('qos_{}_burst'.format(qos_dir), '') + average = request.POST.get('qos_average') or 0 + peak = request.POST.get('qos_peak') or 0 + burst = request.POST.get('qos_burst') or 0 - conn.set_qos(qos_dir, average, peak, burst) - if conn.is_active(): - messages.success(request, "{} Qos is set. Network XML is changed.".format(qos_dir.capitalize()) + - "Stop and start network to activate new config") - else: - messages.success(request, "{} Qos is set".format(qos_dir.capitalize())) + try: + conn.set_qos(qos_dir, average, peak, burst) + if conn.is_active(): + messages.success(request, "{} Qos is set. Network XML is changed.".format(qos_dir.capitalize()) + + "Stop and start network to activate new config") + else: + messages.success(request, "{} Qos is set".format(qos_dir.capitalize())) + except libvirtError as le: + messages.error(request, le.message) return HttpResponseRedirect(request.get_full_path()) if 'unset_qos' in request.POST: qos_dir = request.POST.get('qos_direction', '') diff --git a/static/js/novnc/app/styles/lite.css b/static/js/novnc/app/styles/lite.css index 13e11c7..107bf47 100755 --- a/static/js/novnc/app/styles/lite.css +++ b/static/js/novnc/app/styles/lite.css @@ -21,9 +21,9 @@ html { } #noVNC_status_bar { + margin-top: 52px; width: 100%; - display:flex; - justify-content: space-between; + display: flex; } #noVNC_status { diff --git a/static/js/spice-html5/spice.css b/static/js/spice-html5/spice.css index ee1b2f3..23b7e51 100755 --- a/static/js/spice-html5/spice.css +++ b/static/js/spice-html5/spice.css @@ -81,7 +81,7 @@ body { min-height: 600px; height: 100%; - margin: 10px; + margin: 62px 10px 10px 10px; padding: 0; background-color: #333333; } diff --git a/vrtManager/connection.py b/vrtManager/connection.py index 062ee91..e186854 100644 --- a/vrtManager/connection.py +++ b/vrtManager/connection.py @@ -480,7 +480,7 @@ class wvmConnect(object): """Get available image filename extensions""" return ['img', 'qcow', 'qcow2'] - def get_video(self): + def get_video_models(self): """ Get available graphics video types """ def get_video_list(ctx): result = [] @@ -508,6 +508,15 @@ class wvmConnect(object): def get_network(self, net): return self.wvm.networkLookupByName(net) + def get_network_forward(self, net_name): + def get_forward(doc): + forward_mode = util.get_xpath(doc, '/network/forward/@mode') + return forward_mode or 'isolated' + + net = self.get_network(net_name) + xml = net.XMLDesc(0) + return util.get_xml_path(xml, func=get_forward) + def get_nwfilter(self, name): return self.wvm.nwfilterLookupByName(name) diff --git a/vrtManager/instance.py b/vrtManager/instance.py index c929160..9fe7d67 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -7,6 +7,7 @@ except: from libvirt import libvirtError, VIR_DOMAIN_XML_SECURE, VIR_MIGRATE_LIVE from vrtManager import util from xml.etree import ElementTree +from lxml import etree from datetime import datetime from vrtManager.connection import wvmConnect from vrtManager.storage import wvmStorage @@ -157,6 +158,12 @@ class wvmInstance(wvmConnect): return self.wvm.defineXML(xml) def get_status(self): + """ + VIR_DOMAIN_NOSTATE = 0 + VIR_DOMAIN_RUNNING = 1 + VIR_DOMAIN_PAUSED = 3 + VIR_DOMAIN_SHUTOFF = 5 + """ return self.instance.info()[0] def get_autostart(self): @@ -229,17 +236,38 @@ class wvmInstance(wvmConnect): def networks(ctx): result = [] + inbound = outbound = [] for net in ctx.xpath('/domain/devices/interface'): - mac_host = net.xpath('mac/@address')[0] - network_host = net.xpath('source/@network|source/@bridge|source/@dev')[0] - target_host = '' if not net.xpath('target/@dev') else net.xpath('target/@dev')[0] - filterref_host = '' if not net.xpath('filterref/@filter') else net.xpath('filterref/@filter')[0] + mac_inst = net.xpath('mac/@address')[0] + nic_inst = net.xpath('source/@network|source/@bridge|source/@dev')[0] + target_inst = '' if not net.xpath('target/@dev') else net.xpath('target/@dev')[0] + filterref_inst = '' if not net.xpath('filterref/@filter') else net.xpath('filterref/@filter')[0] + if net.xpath('bandwidth/inbound'): + in_attr = net.xpath('bandwidth/inbound')[0] + in_av = in_attr.get('average') + in_peak = in_attr.get('peak') + in_burst = in_attr.get('burst') + inbound = {'average': in_av, 'peak': in_peak, 'burst': in_burst} + if net.xpath('bandwidth/outbound'): + out_attr = net.xpath('bandwidth/outbound')[0] + out_av = out_attr.get('average') + out_peak = out_attr.get('peak') + out_burst = out_attr.get('burst') + outbound = {'average': out_av, 'peak': out_peak, 'burst': out_burst} + try: - net = self.get_network(network_host) - ip = get_mac_ipaddr(net, mac_host) + net = self.get_network(nic_inst) + ip = get_mac_ipaddr(net, mac_inst) except libvirtError: ip = None - result.append({'mac': mac_host, 'nic': network_host, 'target': target_host, 'ip': ip, 'filterref': filterref_host}) + result.append({'mac': mac_inst, + 'nic': nic_inst, + 'target': target_inst, + 'ip': ip, + 'filterref': filterref_inst, + 'inbound': inbound, + 'outbound': outbound, + }) return result return util.get_xml_path(self._XMLDesc(0), func=networks) @@ -680,8 +708,7 @@ class wvmInstance(wvmConnect): def get_console_port(self, console_type=None): if console_type is None: console_type = self.get_console_type() - port = util.get_xml_path(self._XMLDesc(0), - "/domain/devices/graphics[@type='%s']/@port" % console_type) + port = util.get_xml_path(self._XMLDesc(0), "/domain/devices/graphics[@type='%s']/@port" % console_type) return port def get_console_websocket_port(self): @@ -691,8 +718,7 @@ class wvmInstance(wvmConnect): return websocket_port def get_console_passwd(self): - return util.get_xml_path(self._XMLDesc(VIR_DOMAIN_XML_SECURE), - "/domain/devices/graphics/@passwd") + return util.get_xml_path(self._XMLDesc(VIR_DOMAIN_XML_SECURE), "/domain/devices/graphics/@passwd") def set_console_passwd(self, passwd): xml = self._XMLDesc(VIR_DOMAIN_XML_SECURE) @@ -735,8 +761,29 @@ class wvmInstance(wvmConnect): self._defineXML(newxml) def get_console_keymap(self): - return util.get_xml_path(self._XMLDesc(VIR_DOMAIN_XML_SECURE), - "/domain/devices/graphics/@keymap") or '' + return util.get_xml_path(self._XMLDesc(VIR_DOMAIN_XML_SECURE), "/domain/devices/graphics/@keymap") or '' + + def get_video_model(self): + """ :return only primary video card""" + xml = self._XMLDesc(VIR_DOMAIN_XML_SECURE) + tree = etree.fromstring(xml) + video_models = tree.xpath("/domain/devices/video/model") + for model in video_models: + if model.get('primary') == 'yes' or len(video_models) == 1: + return model.get('type') + + def set_video_model(self, model): + """ Changes only primary video card""" + xml = self._XMLDesc(VIR_DOMAIN_XML_SECURE) + tree = etree.fromstring(xml) + video_models = tree.xpath("/domain/devices/video/model") + video_xml = "<model type='{}'/>".format(model) + for model in video_models: + if model.get('primary') == 'yes' or len(video_models) == 1: + parent = model.getparent() + parent.remove(model) + parent.append(etree.fromstring(video_xml)) + self._defineXML(etree.tostring(tree)) def resize(self, cur_memory, memory, cur_vcpu, vcpu, disks=[]): """ @@ -1007,25 +1054,34 @@ class wvmInstance(wvmConnect): bridge_name = net.bridgeName() return bridge_name - def add_network(self, mac_address, source, source_type='net', interface_type='bridge', model='virtio', nwfilter=None): + def add_network(self, mac_address, source, source_type='net', model='virtio', nwfilter=None): bridge_name = self.get_bridge_name(source, source_type) - xml_interface = """ - <interface type='%s'> - <mac address='%s'/> - <source bridge='%s'/> - <model type='%s'/> - """ % (interface_type, mac_address, bridge_name, model) + + forward_mode = self.get_network_forward(source) + if forward_mode in ['nat', 'isolated', 'routed']: + interface_type = 'network' + else: + interface_type = 'bridge' + + xml_iface = """ + <interface type='%s'> + <mac address='%s'/>""" % (interface_type, mac_address) + if interface_type == 'network': + xml_iface += """<source network='%s'/>""" % source + else: + xml_iface += """<source bridge='%s'/>""" % bridge_name + xml_iface += """<model type='%s'/>""" % model if nwfilter: - xml_interface += """ + xml_iface += """ <filterref filter='%s'/> """ % nwfilter - xml_interface += """</interface>""" + xml_iface += """</interface>""" if self.get_status() == 1: - self.instance.attachDeviceFlags(xml_interface, VIR_DOMAIN_AFFECT_LIVE) - self.instance.attachDeviceFlags(xml_interface, VIR_DOMAIN_AFFECT_CONFIG) + self.instance.attachDeviceFlags(xml_iface, VIR_DOMAIN_AFFECT_LIVE) + self.instance.attachDeviceFlags(xml_iface, VIR_DOMAIN_AFFECT_CONFIG) if self.get_status() == 5: - self.instance.attachDeviceFlags(xml_interface, VIR_DOMAIN_AFFECT_CONFIG) + self.instance.attachDeviceFlags(xml_iface, VIR_DOMAIN_AFFECT_CONFIG) def delete_network(self, mac_address): tree = ElementTree.fromstring(self._XMLDesc(0)) @@ -1044,10 +1100,11 @@ class wvmInstance(wvmConnect): xml = self._XMLDesc(VIR_DOMAIN_XML_SECURE) tree = ElementTree.fromstring(xml) for num, interface in enumerate(tree.findall('devices/interface')): - net_source = network_data['net-source-' + str(num)] - net_source_type = network_data['net-source-' + str(num) + '-type'] - net_mac = network_data['net-mac-' + str(num)] - net_filter = network_data['net-nwfilter-' + str(num)] + net_mac = network_data.get('net-mac-' + str(num)) + if net_mac is None: continue + net_source = network_data.get('net-source-' + str(num)) + net_source_type = network_data.get('net-source-' + str(num) + '-type') + net_filter = network_data.get('net-nwfilter-' + str(num)) bridge_name = self.get_bridge_name(net_source, net_source_type) if interface.get('type') == 'bridge': source = interface.find('mac') @@ -1103,10 +1160,78 @@ class wvmInstance(wvmConnect): tree = ElementTree.fromstring(xml) self._set_options(tree, options) - new_xml = ElementTree.tostring(tree) self._defineXML(new_xml) def set_memory(self, size, flags=0): self.instance.setMemoryFlags(size, flags) + def get_all_qos(self): + qos_values = dict() + tree = etree.fromstring(self._XMLDesc(0)) + qos = tree.xpath("/domain/devices/interface") + + for q in qos: + bound_list = list() + mac = q.xpath('mac/@address') + band = q.find('bandwidth') + if band is not None: + in_qos = band.find('inbound') + if in_qos is not None: + in_av = in_qos.get('average') + in_peak = in_qos.get('peak') + in_burst = in_qos.get('burst') + in_floor = in_qos.get('floor') + bound_list.append({'direction': 'inbound', 'average': in_av, 'peak': in_peak, 'floor': in_floor, 'burst': in_burst}) + + out_qos = band.find('outbound') + if out_qos is not None: + out_av = out_qos.get('average') + out_peak = out_qos.get('peak') + out_burst = out_qos.get('burst') + bound_list.append({'direction': 'outbound', 'average': out_av, 'peak': out_peak, 'burst': out_burst}) + qos_values[mac[0]] = bound_list + return qos_values + + def set_qos(self, mac, direction, average, peak, burst): + if direction == "inbound": + xml = "<inbound average='{}' peak='{}' burst='{}'/>".format(average, peak, burst) + elif direction == "outbound": + xml = "<outbound average='{}' peak='{}' burst='{}'/>".format(average, peak, burst) + else: + raise Exception('Direction must be inbound or outbound') + + tree = etree.fromstring(self._XMLDesc(0)) + + macs = tree.xpath("/domain/devices/interface/mac") + for cur_mac in macs: + + if cur_mac.get("address") == mac: + interface = cur_mac.getparent() + band = interface.find('bandwidth') + if band is None: + xml = "<bandwidth>" + xml + "</bandwidth>" + interface.append(etree.fromstring(xml)) + else: + direct = band.find(direction) + if direct is not None: + parent = direct.getparent() + parent.remove(direct) + parent.append(etree.fromstring(xml)) + else: + band.append(etree.fromstring(xml)) + new_xml = etree.tostring(tree) + self.wvm.defineXML(new_xml) + + def unset_qos(self, mac, direction): + tree = etree.fromstring(self._XMLDesc(0)) + for direct in tree.xpath("/domain/devices/interface/bandwidth/{}".format(direction)): + band_el = direct.getparent() + interface_el = band_el.getparent() # parent bandwidth,it parent is interface + parent_mac = interface_el.xpath('mac/@address') + if parent_mac[0] == mac: + band_el.remove(direct) + + self.wvm.defineXML(etree.tostring(tree)) + +