mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-21 14:50:08 +03:00
Signed-off-by: Frederick Borges <fborges@opennebula.io>
This commit is contained in:
parent
dc999c347b
commit
74924f4138
@ -102,6 +102,9 @@ features:
|
||||
|
||||
# True to show schedule actions section to instantiate VM
|
||||
show_sched_actions_instantiate: true
|
||||
|
||||
# True to show boot order section to instantiate VM
|
||||
show_boot_order: true
|
||||
tabs:
|
||||
dashboard-tab:
|
||||
# The following widgets can be used inside any of the '_per_row' settings
|
||||
|
@ -44,6 +44,9 @@ features:
|
||||
|
||||
# True to show schedule actions section to instantiate VM
|
||||
show_sched_actions_instantiate: true
|
||||
|
||||
# True to show boot order section to instantiate VM
|
||||
show_boot_order: true
|
||||
tabs:
|
||||
provision-tab:
|
||||
panel_tabs:
|
||||
|
@ -252,7 +252,7 @@ tabs:
|
||||
Template.refresh: true
|
||||
Template.create_dialog: false
|
||||
Template.import_dialog: false
|
||||
Template.update_dialog: false
|
||||
Template.update_dialog: true
|
||||
Template.instantiate_vms: true
|
||||
Template.rename: true
|
||||
Template.chown: true
|
||||
|
@ -96,6 +96,9 @@ features:
|
||||
|
||||
# True to show schedule actions section to instantiate VM
|
||||
show_sched_actions_instantiate: true
|
||||
|
||||
# True to show boot order section to instantiate VM
|
||||
show_boot_order: true
|
||||
tabs:
|
||||
dashboard-tab:
|
||||
# The following widgets can be used inside any of the '_per_row' settings
|
||||
|
@ -102,6 +102,9 @@ features:
|
||||
|
||||
# True to show schedule actions to instantiate a VM
|
||||
show_sched_actions_instantiate: true
|
||||
|
||||
# True to show boot order section to instantiate VM
|
||||
show_boot_order: true
|
||||
tabs:
|
||||
dashboard-tab:
|
||||
# The following widgets can be used inside any of the '_per_row' settings
|
||||
|
@ -44,6 +44,9 @@ features:
|
||||
|
||||
# True to show schedule actions section to instantiate VM
|
||||
show_sched_actions_instantiate: true
|
||||
|
||||
# True to show boot order section to instantiate VM
|
||||
show_boot_order: true
|
||||
tabs:
|
||||
provision-tab:
|
||||
panel_tabs:
|
||||
|
@ -102,6 +102,9 @@ features:
|
||||
|
||||
# True to show schedule actions section to instantiate VM
|
||||
show_sched_actions_instantiate: true
|
||||
|
||||
# True to show boot order section to instantiate VM
|
||||
show_boot_order: true
|
||||
tabs:
|
||||
dashboard-tab:
|
||||
# The following widgets can be used inside any of the '_per_row' settings
|
||||
@ -252,7 +255,7 @@ tabs:
|
||||
Template.refresh: true
|
||||
Template.create_dialog: false
|
||||
Template.import_dialog: false
|
||||
Template.update_dialog: false
|
||||
Template.update_dialog: true
|
||||
Template.instantiate_vms: true
|
||||
Template.rename: true
|
||||
Template.chown: true
|
||||
|
@ -96,6 +96,9 @@ features:
|
||||
|
||||
# True to show schedule actions section to instantiate VM
|
||||
show_sched_actions_instantiate: true
|
||||
|
||||
# True to show boot order section to instantiate VM
|
||||
show_boot_order: true
|
||||
tabs:
|
||||
dashboard-tab:
|
||||
# The following widgets can be used inside any of the '_per_row' settings
|
||||
|
@ -102,6 +102,9 @@ features:
|
||||
|
||||
# True to show schedule actions section to instantiate VM
|
||||
show_sched_actions_instantiate: true
|
||||
|
||||
# True to show boot order section to instantiate VM
|
||||
show_boot_order: true
|
||||
tabs:
|
||||
dashboard-tab:
|
||||
# The following widgets can be used inside any of the '_per_row' settings
|
||||
|
@ -44,6 +44,9 @@ features:
|
||||
|
||||
# True to show schedule actions section to instantiate VM
|
||||
show_sched_actions_instantiate: true
|
||||
|
||||
# True to show boot order section to instantiate VM
|
||||
show_boot_order: true
|
||||
tabs:
|
||||
provision-tab:
|
||||
panel_tabs:
|
||||
|
@ -102,6 +102,9 @@ features:
|
||||
|
||||
# True to show schedule actions section to instantiate VM
|
||||
show_sched_actions_instantiate: true
|
||||
|
||||
# True to show boot order section to instantiate VM
|
||||
show_boot_order: true
|
||||
tabs:
|
||||
dashboard-tab:
|
||||
# The following widgets can be used inside any of the '_per_row' settings
|
||||
@ -252,7 +255,7 @@ tabs:
|
||||
Template.refresh: true
|
||||
Template.create_dialog: false
|
||||
Template.import_dialog: false
|
||||
Template.update_dialog: false
|
||||
Template.update_dialog: true
|
||||
Template.instantiate_vms: true
|
||||
Template.rename: true
|
||||
Template.chown: true
|
||||
|
@ -96,6 +96,9 @@ features:
|
||||
|
||||
# True to show schedule actions section to instantiate VM
|
||||
show_sched_actions_instantiate: true
|
||||
|
||||
# True to show boot order section to instantiate VM
|
||||
show_boot_order: true
|
||||
tabs:
|
||||
dashboard-tab:
|
||||
# The following widgets can be used inside any of the '_per_row' settings
|
||||
|
@ -51,6 +51,11 @@ define(function(require) {
|
||||
var TemplateDashboardGroupVms = require("hbs!./provision-tab/dashboard/group-vms");
|
||||
var TAB_ID = require("./provision-tab/tabId");
|
||||
var FLOW_TEMPLATE_LABELS_COLUMN = 2;
|
||||
|
||||
var distinct = function(value, index, self){
|
||||
return self.indexOf(value)===index;
|
||||
};
|
||||
|
||||
var povision_actions = {
|
||||
"Provision.Flow.instantiate" : {
|
||||
type: "single",
|
||||
@ -632,6 +637,7 @@ define(function(require) {
|
||||
$("#provision_create_vm .provision_add_vmgroup").show();
|
||||
$("#provision_create_vm .provision_vmgroup").hide();
|
||||
$("#provision_create_vm .provision_ds").hide();
|
||||
$("#provision_create_vm .provision_boot").hide();
|
||||
$("#provision_create_vm .provision_custom_attributes_selector").html("");
|
||||
$("#provision_create_vm li:not(.is-active) a[href='#provision_dd_template']").trigger("click");
|
||||
$("#provision_create_vm .total_cost_div").hide();
|
||||
@ -851,6 +857,153 @@ define(function(require) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Boot order
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
function _retrieveBootValue(context) {
|
||||
return $("table.boot-order-instantiate-provision", context).attr("value");
|
||||
}
|
||||
|
||||
function _fillBootValue(context, value) {
|
||||
return $("table.boot-order-instantiate-provision", context).attr("value", value);
|
||||
}
|
||||
|
||||
function _refreshBootValue(context) {
|
||||
var table = $("table.boot-order-instantiate-provision", context);
|
||||
|
||||
var devices = [];
|
||||
|
||||
$.each($("tr", table), function(){
|
||||
if ($("input", this).is(":checked")){
|
||||
devices.push( $(this).attr("value") );
|
||||
}
|
||||
});
|
||||
|
||||
table.attr("value", devices.join(","));
|
||||
}
|
||||
|
||||
function _addBootRow(context, value, label) {
|
||||
$("table.boot-order-instantiate-provision tbody", context).append(
|
||||
"<tr value=\""+value+"\">"+
|
||||
"<td><input type=\"checkbox\"/></td>"+
|
||||
"<td>"+value+"</td>"+
|
||||
"<td><label>"+label+"</label></td>"+
|
||||
"<td>"+
|
||||
"<button class=\"boot-order-instantiate-provision-up button radius tiny secondary\"><i class=\"fas fa-lg fa-arrow-up\" aria-hidden=\"true\"></i></button>"+
|
||||
"<button class=\"boot-order-instantiate-provision-down button radius tiny secondary\"><i class=\"fas fa-lg fa-arrow-down\" aria-hidden=\"true\"></i></button>"+
|
||||
"</td>"+
|
||||
"</tr>");
|
||||
}
|
||||
|
||||
function _loadBootOrder(context, templateJSON) {
|
||||
var table = $("table.boot-order-instantiate-provision", context);
|
||||
var prev_value = $(table).attr("value");
|
||||
|
||||
$("table.boot-order-instantiate-provision tbody", context).html("");
|
||||
|
||||
if (templateJSON.DISK !== undefined){
|
||||
var disks = templateJSON.DISK;
|
||||
|
||||
if (!$.isArray(disks)){
|
||||
disks = [disks];
|
||||
}
|
||||
disks = disks.filter(distinct);
|
||||
|
||||
$.each(disks, function(i,disk){
|
||||
var label = "<i class=\"fas fa-fw fa-lg fa-server\"></i> ";
|
||||
var disk_name = "disk";
|
||||
|
||||
if (disk.IMAGE !== undefined){
|
||||
label += disk.IMAGE;
|
||||
} else if (disk.IMAGE_ID !== undefined){
|
||||
label += Locale.tr("Image ID") + " " + disk.IMAGE_ID;
|
||||
} else {
|
||||
label += Locale.tr("Volatile");
|
||||
}
|
||||
|
||||
if (disk.DISK_ID === undefined){
|
||||
disk_name += i;
|
||||
} else {
|
||||
disk_name += disk.DISK_ID;
|
||||
}
|
||||
|
||||
_addBootRow(context, disk_name, label);
|
||||
});
|
||||
}
|
||||
|
||||
if (templateJSON.NIC !== undefined){
|
||||
var nics = templateJSON.NIC;
|
||||
|
||||
if (!$.isArray(nics)){
|
||||
nics = [nics];
|
||||
}
|
||||
nics = nics.filter(distinct);
|
||||
nics.map(function(nic,i){
|
||||
var label = "<i class=\"fas fa-fw fa-lg fa-globe\"></i> ";
|
||||
if (nic && nic.NETWORK && nic.NETWORK !== undefined){
|
||||
label += nic.NETWORK;
|
||||
} else if (nic.NETWORK_ID !== undefined){
|
||||
label += Locale.tr("Network ID") + " " + nic.NETWORK_ID;
|
||||
} else {
|
||||
label += Locale.tr("Manual settings");
|
||||
}
|
||||
_addBootRow(context, "nic"+i, label);
|
||||
});
|
||||
}
|
||||
|
||||
if (templateJSON.DISK === undefined && templateJSON.NIC === undefined){
|
||||
$("table.boot-order-instantiate-provision tbody", context).append(
|
||||
"<tr>\
|
||||
<td>" + Locale.tr("Disks and NICs will appear here") + "</td>\
|
||||
</tr>");
|
||||
}
|
||||
|
||||
if (prev_value.length > 0){
|
||||
var pos = 0;
|
||||
|
||||
$.each(prev_value.split(","), function(i,device){
|
||||
var tr = $("tr[value=\"" + device + "\"]", table);
|
||||
|
||||
if(tr.length > 0){
|
||||
$($("tr", table)[pos]).before(tr);
|
||||
$("input", tr).click();
|
||||
|
||||
pos += 1;
|
||||
}
|
||||
});
|
||||
|
||||
_refreshBootValue(context);
|
||||
}
|
||||
}
|
||||
|
||||
tab.on("click", "button.boot-order-instantiate-provision-up", function(){
|
||||
var tr = $(this).closest("tr");
|
||||
tr.prev().before(tr);
|
||||
|
||||
_refreshBootValue(tab);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
tab.on("click", "button.boot-order-instantiate-provision-down", function(){
|
||||
var tr = $(this).closest("tr");
|
||||
tr.next().after(tr);
|
||||
|
||||
_refreshBootValue(tab);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$("table.boot-order-instantiate-provision tbody", tab).on("change", "input", function(){
|
||||
_refreshBootValue(tab);
|
||||
});
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// End Boot order
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
provision_vm_instantiate_templates_datatable = $("#provision_vm_instantiate_templates_table").dataTable({
|
||||
"iDisplayLength": 6,
|
||||
"bAutoWidth": false,
|
||||
@ -965,6 +1118,7 @@ define(function(require) {
|
||||
|
||||
$("#provision_create_vm .provision_vmgroup").show();
|
||||
$("#provision_create_vm .provision_ds").show();
|
||||
$("#provision_create_vm .provision_boot").show();
|
||||
|
||||
OpenNebula.Template.show({
|
||||
data : {
|
||||
@ -974,6 +1128,7 @@ define(function(require) {
|
||||
timeout: true,
|
||||
success: function (request, template_json) {
|
||||
that.template_base_json= template_json;
|
||||
tab.template_base_json = template_json;
|
||||
}
|
||||
});
|
||||
|
||||
@ -1077,6 +1232,15 @@ define(function(require) {
|
||||
$(".provision_custom_attributes_selector", create_vm_context).html("");
|
||||
}
|
||||
|
||||
// boot order
|
||||
|
||||
var osJSON = template_json.VMTEMPLATE.TEMPLATE.OS;
|
||||
if (osJSON && osJSON["BOOT"]) {
|
||||
_fillBootValue(create_vm_context, osJSON["BOOT"]);
|
||||
}
|
||||
|
||||
_loadBootOrder(create_vm_context, template_json.VMTEMPLATE.TEMPLATE)
|
||||
|
||||
},
|
||||
error: function(request, error_json, container) {
|
||||
Notifier.onError(request, error_json, container);
|
||||
@ -1168,6 +1332,16 @@ define(function(require) {
|
||||
extra_info.template.TOPOLOGY = topology;
|
||||
}
|
||||
|
||||
var boot = _retrieveBootValue(context);
|
||||
var os = tab.template_base_json.VMTEMPLATE.TEMPLATE.OS ? tab.template_base_json.VMTEMPLATE.TEMPLATE.OS : {};
|
||||
|
||||
if (boot && boot.length > 0) {
|
||||
os.BOOT = boot
|
||||
extra_info.template.OS = os;
|
||||
} else {
|
||||
extra_info.template.OS = os;
|
||||
}
|
||||
|
||||
var action;
|
||||
|
||||
if ($("input.instantiate_pers", context).prop("checked")){
|
||||
|
@ -150,4 +150,25 @@
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
{{#isFeatureEnabled "show_boot_order"}}
|
||||
<div class="row provision_boot" hidden="true">
|
||||
<div class="small-12 columns bootContext{{element.ID}}">
|
||||
<fieldset>
|
||||
<legend>
|
||||
<i class="fas fa-power-off"></i> {{tr "OS Booting"}}
|
||||
</legend>
|
||||
<div class="provision_boot_selector" data-tab-content>
|
||||
<label>
|
||||
{{tr "Boot order"}}
|
||||
{{{tip (tr "Select the devices to boot from, and their order")}}}
|
||||
</label>
|
||||
<table class="boot-order-instantiate-provision dataTable" value="">
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
{{/isFeatureEnabled}}
|
||||
</form>
|
@ -55,6 +55,10 @@ define(function(require) {
|
||||
var CREATE = true;
|
||||
var contextRow;
|
||||
|
||||
var distinct = function(value, index, self){
|
||||
return self.indexOf(value)===index;
|
||||
};
|
||||
|
||||
/*
|
||||
CONSTRUCTOR
|
||||
*/
|
||||
@ -200,6 +204,32 @@ define(function(require) {
|
||||
ScheduleActions.fill($(this),context);
|
||||
}
|
||||
});
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Boot order
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
context.on("click", "button.boot-order-instantiate-up", function(){
|
||||
var tr = $(this).closest("tr");
|
||||
tr.prev().before(tr);
|
||||
|
||||
_refreshBootValue(context);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
context.on("click", "button.boot-order-instantiate-down", function(){
|
||||
var tr = $(this).closest("tr");
|
||||
tr.next().after(tr);
|
||||
|
||||
_refreshBootValue(context);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$("table.boot-order-instantiate tbody", context).on("change", "input", function(){
|
||||
_refreshBootValue(context);
|
||||
});
|
||||
}
|
||||
|
||||
function _calculateCost(){
|
||||
@ -428,6 +458,16 @@ define(function(require) {
|
||||
tmp_json.TOPOLOGY = topology;
|
||||
}
|
||||
|
||||
var boot = _retrieveBootValue(context);
|
||||
var os = original_tmpl.TEMPLATE.OS ? original_tmpl.TEMPLATE.OS : {};
|
||||
|
||||
if (boot && boot.length > 0) {
|
||||
os.BOOT = boot
|
||||
tmp_json.OS = os;
|
||||
} else {
|
||||
tmp_json.OS = os;
|
||||
}
|
||||
|
||||
extra_info["template"] = tmp_json;
|
||||
for (var i = 0; i < n_times_int; i++) {
|
||||
extra_info["vm_name"] = vm_name.replace(/%i/gi, i); // replace wildcard
|
||||
@ -679,6 +719,13 @@ define(function(require) {
|
||||
if (idsLength == idsDone){
|
||||
Sunstone.enableFormPanelSubmit(that.tabId);
|
||||
}
|
||||
|
||||
var osJSON = template_json.VMTEMPLATE.TEMPLATE.OS;
|
||||
if (osJSON && osJSON["BOOT"]) {
|
||||
_fillBootValue(context, osJSON["BOOT"]);
|
||||
}
|
||||
|
||||
_loadBootOrder(context, template_json.VMTEMPLATE.TEMPLATE)
|
||||
},
|
||||
error: function(request, error_json, container) {
|
||||
Notifier.onError(request, error_json, container);
|
||||
@ -726,4 +773,124 @@ define(function(require) {
|
||||
$("#SCHED_REQUIREMENTS" + id, context).val(req_string.join(" | "));
|
||||
$("#SCHED_DS_REQUIREMENTS" + id, context).val(req_ds_string.join(" | "));
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Boot order
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
function _retrieveBootValue(context) {
|
||||
return $("table.boot-order-instantiate", context).attr("value");
|
||||
}
|
||||
|
||||
function _fillBootValue(context, value) {
|
||||
return $("table.boot-order-instantiate", context).attr("value", value);
|
||||
}
|
||||
|
||||
function _refreshBootValue(context) {
|
||||
var table = $("table.boot-order-instantiate", context);
|
||||
|
||||
var devices = [];
|
||||
|
||||
$.each($("tr", table), function(){
|
||||
if ($("input", this).is(":checked")){
|
||||
devices.push( $(this).attr("value") );
|
||||
}
|
||||
});
|
||||
|
||||
table.attr("value", devices.join(","));
|
||||
}
|
||||
|
||||
function _addBootRow(context, value, label) {
|
||||
$("table.boot-order-instantiate tbody", context).append(
|
||||
"<tr value=\""+value+"\">"+
|
||||
"<td><input type=\"checkbox\"/></td>"+
|
||||
"<td>"+value+"</td>"+
|
||||
"<td><label>"+label+"</label></td>"+
|
||||
"<td>"+
|
||||
"<button class=\"boot-order-instantiate-up button radius tiny secondary\"><i class=\"fas fa-lg fa-arrow-up\" aria-hidden=\"true\"></i></button>"+
|
||||
"<button class=\"boot-order-instantiate-down button radius tiny secondary\"><i class=\"fas fa-lg fa-arrow-down\" aria-hidden=\"true\"></i></button>"+
|
||||
"</td>"+
|
||||
"</tr>");
|
||||
}
|
||||
|
||||
function _loadBootOrder(context, templateJSON) {
|
||||
var table = $("table.boot-order-instantiate", context);
|
||||
var prev_value = $(table).attr("value");
|
||||
|
||||
$("table.boot-order-instantiate tbody", context).html("");
|
||||
|
||||
if (templateJSON.DISK !== undefined){
|
||||
var disks = templateJSON.DISK;
|
||||
|
||||
if (!$.isArray(disks)){
|
||||
disks = [disks];
|
||||
}
|
||||
disks = disks.filter(distinct);
|
||||
|
||||
$.each(disks, function(i,disk){
|
||||
var label = "<i class=\"fas fa-fw fa-lg fa-server\"></i> ";
|
||||
var disk_name = "disk";
|
||||
|
||||
if (disk.IMAGE !== undefined){
|
||||
label += disk.IMAGE;
|
||||
} else if (disk.IMAGE_ID !== undefined){
|
||||
label += Locale.tr("Image ID") + " " + disk.IMAGE_ID;
|
||||
} else {
|
||||
label += Locale.tr("Volatile");
|
||||
}
|
||||
|
||||
if (disk.DISK_ID === undefined){
|
||||
disk_name += i;
|
||||
} else {
|
||||
disk_name += disk.DISK_ID;
|
||||
}
|
||||
|
||||
_addBootRow(context, disk_name, label);
|
||||
});
|
||||
}
|
||||
|
||||
if (templateJSON.NIC !== undefined){
|
||||
var nics = templateJSON.NIC;
|
||||
|
||||
if (!$.isArray(nics)){
|
||||
nics = [nics];
|
||||
}
|
||||
nics = nics.filter(distinct);
|
||||
nics.map(function(nic,i){
|
||||
var label = "<i class=\"fas fa-fw fa-lg fa-globe\"></i> ";
|
||||
if (nic && nic.NETWORK && nic.NETWORK !== undefined){
|
||||
label += nic.NETWORK;
|
||||
} else if (nic.NETWORK_ID !== undefined){
|
||||
label += Locale.tr("Network ID") + " " + nic.NETWORK_ID;
|
||||
} else {
|
||||
label += Locale.tr("Manual settings");
|
||||
}
|
||||
_addBootRow(context, "nic"+i, label);
|
||||
});
|
||||
}
|
||||
|
||||
if (templateJSON.DISK === undefined && templateJSON.NIC === undefined){
|
||||
$("table.boot-order-instantiate tbody", context).append(
|
||||
"<tr>\
|
||||
<td>" + Locale.tr("Disks and NICs will appear here") + "</td>\
|
||||
</tr>");
|
||||
}
|
||||
|
||||
if (prev_value.length > 0){
|
||||
var pos = 0;
|
||||
|
||||
$.each(prev_value.split(","), function(i,device){
|
||||
var tr = $("tr[value=\"" + device + "\"]", table);
|
||||
|
||||
if(tr.length > 0){
|
||||
$($("tr", table)[pos]).before(tr);
|
||||
$("input", tr).click();
|
||||
|
||||
pos += 1;
|
||||
}
|
||||
});
|
||||
|
||||
_refreshBootValue(context);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -158,5 +158,21 @@
|
||||
</div>
|
||||
{{/advancedImportationSection}}
|
||||
{{/isFeatureEnabled}}
|
||||
{{#isFeatureEnabled "show_boot_order"}}
|
||||
{{#advancedImportationSection "<i class=\"fas fa-power-off\"></i>" (tr "OS Booting")}}
|
||||
<div class="row">
|
||||
<div class="medium-8 columns">
|
||||
<label>
|
||||
{{tr "Boot order"}}
|
||||
{{{tip (tr "Select the devices to boot from, and their order")}}}
|
||||
</label>
|
||||
<table class="boot-order-instantiate dataTable" value="">
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{/advancedImportationSection}}
|
||||
{{/isFeatureEnabled}}
|
||||
</div>
|
||||
<br>
|
Loading…
x
Reference in New Issue
Block a user