mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-23 22:50:09 +03:00
Merge remote-tracking branch 'origin/master' into feature-1727
This commit is contained in:
commit
a7f3e02908
@ -1908,7 +1908,7 @@ do_file() {
|
||||
if [ "$LINK" = "yes" ]; then
|
||||
ln -s $SRC_DIR/$1 $DESTDIR$2
|
||||
else
|
||||
cp -R $SRC_DIR/$1 $DESTDIR$2
|
||||
cp -RL $SRC_DIR/$1 $DESTDIR$2
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
@ -16,7 +16,15 @@
|
||||
# limitations under the License. #
|
||||
# -------------------------------------------------------------------------- #
|
||||
|
||||
$: << File.join(File.dirname(__FILE__), '../../vmm/az')
|
||||
ONE_LOCATION=ENV["ONE_LOCATION"] if !defined?(ONE_LOCATION)
|
||||
|
||||
if !ONE_LOCATION
|
||||
RUBY_LIB_LOCATION="/usr/lib/one/ruby" if !defined?(RUBY_LIB_LOCATION)
|
||||
else
|
||||
RUBY_LIB_LOCATION=ONE_LOCATION+"/lib/ruby" if !defined?(RUBY_LIB_LOCATION)
|
||||
end
|
||||
|
||||
$: << RUBY_LIB_LOCATION
|
||||
|
||||
require 'az_driver'
|
||||
|
||||
|
@ -16,7 +16,15 @@
|
||||
# limitations under the License. #
|
||||
# -------------------------------------------------------------------------- #
|
||||
|
||||
$: << File.join(File.dirname(__FILE__), '../../vmm/ec2')
|
||||
ONE_LOCATION=ENV["ONE_LOCATION"] if !defined?(ONE_LOCATION)
|
||||
|
||||
if !ONE_LOCATION
|
||||
RUBY_LIB_LOCATION="/usr/lib/one/ruby" if !defined?(RUBY_LIB_LOCATION)
|
||||
else
|
||||
RUBY_LIB_LOCATION=ONE_LOCATION+"/lib/ruby" if !defined?(RUBY_LIB_LOCATION)
|
||||
end
|
||||
|
||||
$: << RUBY_LIB_LOCATION
|
||||
|
||||
require 'ec2_driver'
|
||||
|
||||
|
@ -16,7 +16,15 @@
|
||||
# limitations under the License. #
|
||||
# -------------------------------------------------------------------------- #
|
||||
|
||||
$: << File.join(File.dirname(__FILE__), '../../vmm/sl')
|
||||
ONE_LOCATION=ENV["ONE_LOCATION"] if !defined?(ONE_LOCATION)
|
||||
|
||||
if !ONE_LOCATION
|
||||
RUBY_LIB_LOCATION="/usr/lib/one/ruby" if !defined?(RUBY_LIB_LOCATION)
|
||||
else
|
||||
RUBY_LIB_LOCATION=ONE_LOCATION+"/lib/ruby" if !defined?(RUBY_LIB_LOCATION)
|
||||
end
|
||||
|
||||
$: << RUBY_LIB_LOCATION
|
||||
|
||||
require 'sl_driver'
|
||||
|
||||
|
@ -50,6 +50,10 @@ module OpenNebulaJSON
|
||||
when "instantiate" then self.instantiate(action_hash['params'])
|
||||
when "clone" then self.clone(action_hash['params'])
|
||||
when "rename" then self.rename(action_hash['params'])
|
||||
when "delete_from_provision"
|
||||
then self.delete_from_provision(action_hash['params'])
|
||||
when "chmod_from_provision"
|
||||
then self.chmod_from_provision(action_hash['params'])
|
||||
else
|
||||
error_msg = "#{action_hash['perform']} action not " <<
|
||||
" available for this resource"
|
||||
@ -115,5 +119,37 @@ module OpenNebulaJSON
|
||||
def rename(params=Hash.new)
|
||||
super(params['name'])
|
||||
end
|
||||
|
||||
def delete_from_provision(params=Hash.new)
|
||||
# Delete associated images
|
||||
self.each("TEMPLATE/DISK/IMAGE_ID"){|image_id|
|
||||
img = OpenNebula::Image.new_with_id(image_id.text, @client)
|
||||
rc = img.delete
|
||||
if OpenNebula::is_error?(rc)
|
||||
error_msg = "Some of the resources associated with " <<
|
||||
"this template couldn't be deleted. Error: " << rc.message
|
||||
return OpenNebula::Error.new(error_msg)
|
||||
end
|
||||
}
|
||||
|
||||
# Delete template
|
||||
self.delete
|
||||
end
|
||||
|
||||
def chmod_from_provision(params=Hash.new)
|
||||
# Chmod associated images
|
||||
self.each("TEMPLATE/DISK/IMAGE_ID"){|image_id|
|
||||
img = OpenNebulaJSON::ImageJSON.new_with_id(image_id.text, @client)
|
||||
rc = img.chmod_json(params)
|
||||
if OpenNebula::is_error?(rc)
|
||||
error_msg = "Some of the resources associated with " <<
|
||||
"this template couldn't be published. Error: " << rc.message
|
||||
return OpenNebula::Error.new(error_msg)
|
||||
end
|
||||
}
|
||||
|
||||
# Chmod template
|
||||
self.chmod_json(params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,30 +1,41 @@
|
||||
Sunstone depnedencies
|
||||
Sunstone dependencies
|
||||
=====================
|
||||
|
||||
1. Install nodejs and npm
|
||||
2. Install the following npm packages:
|
||||
`sudo npm install -g bower`
|
||||
`sudo npm install -g grunt`
|
||||
`sudo npm install -g grunt-cli`
|
||||
|
||||
```
|
||||
sudo npm install -g bower
|
||||
sudo npm install -g grunt
|
||||
sudo npm install -g grunt-cli
|
||||
```
|
||||
|
||||
3. Move to the Sunstone public folder and run:
|
||||
`npm install`
|
||||
`bower install`
|
||||
|
||||
```
|
||||
npm install
|
||||
bower install
|
||||
```
|
||||
|
||||
Building minified JS and CSS files
|
||||
==================================
|
||||
|
||||
4. Run the following command to generate the app.css file in the css folder:
|
||||
`grunt sass`
|
||||
```
|
||||
grunt sass
|
||||
```
|
||||
5. Run the following command to generate the minified js files in the dist foler
|
||||
and the app.min.css in the css folder:
|
||||
`grunt requirejs`
|
||||
```
|
||||
grunt requirejs
|
||||
```
|
||||
|
||||
These are the files generate by the grunt requirejs command:
|
||||
```
|
||||
css
|
||||
app.min.css
|
||||
dist
|
||||
login.js, login.js.map main.js main.js.map
|
||||
login.js, login.js.map main.js main.js.map
|
||||
console
|
||||
spice.js spice.js.map vnc.js vnc.js.map
|
||||
```
|
||||
@ -33,9 +44,11 @@ Scons
|
||||
=====
|
||||
|
||||
Scons includes an option to build the minified JS and CSS files. Steps 1, 2 and 3 have to be performed before running this command
|
||||
`scons sunstone=yes`
|
||||
```
|
||||
scons sunstone=yes
|
||||
```
|
||||
|
||||
Install.sh
|
||||
==========
|
||||
|
||||
By default the install.sh script will install all the files, including the non-minified ones. Providing the -p option, only the minified files will be installed.
|
||||
By default the install.sh script will install all the files, including the non-minified ones. Providing the -p option, only the minified files will be installed.
|
||||
|
@ -11,6 +11,9 @@ define(function(require) {
|
||||
"del" : function(params) {
|
||||
OpenNebulaAction.del(params, RESOURCE);
|
||||
},
|
||||
"delete_from_provision": function(params) {
|
||||
OpenNebulaAction.simple_action(params, RESOURCE, "delete_from_provision");
|
||||
},
|
||||
"list" : function(params) {
|
||||
OpenNebulaAction.list(params, RESOURCE);
|
||||
},
|
||||
@ -27,6 +30,10 @@ define(function(require) {
|
||||
var action_obj = params.data.extra_param;
|
||||
OpenNebulaAction.simple_action(params, RESOURCE, "chmod", action_obj);
|
||||
},
|
||||
"chmod_from_provision": function(params) {
|
||||
var action_obj = params.data.extra_param;
|
||||
OpenNebulaAction.simple_action(params, RESOURCE, "chmod_from_provision", action_obj);
|
||||
},
|
||||
"update" : function(params) {
|
||||
var action_obj = params.data.extra_param;
|
||||
OpenNebulaAction.simple_action(params, RESOURCE, "update", action_obj);
|
||||
|
@ -222,7 +222,6 @@ define(function(require) {
|
||||
context.on("click", ".provision_confirm_delete_template_button", function(){
|
||||
var ul_context = $(this).parents(".provision-pricing-table");
|
||||
var template_id = ul_context.attr("opennebula_id");
|
||||
var image_id = ul_context.attr("saved_to_image_id");
|
||||
var template_name = $(".provision-title", ul_context).text();
|
||||
|
||||
$(".provision_confirm_delete_template_div", context).html(
|
||||
@ -236,7 +235,7 @@ define(function(require) {
|
||||
'</span>'+
|
||||
'</div>'+
|
||||
'<div class="large-3 columns">'+
|
||||
'<a href"#" class="provision_delete_template_button alert button large-12 radius right" style="margin-right: 15px" image_id="'+image_id+'" template_id="'+template_id+'">'+Locale.tr("Delete")+'</a>'+
|
||||
'<a href"#" class="provision_delete_template_button alert button large-12 radius right" style="margin-right: 15px" template_id="'+template_id+'">'+Locale.tr("Delete")+'</a>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'<a href="#" class="close">×</a>'+
|
||||
@ -244,55 +243,25 @@ define(function(require) {
|
||||
});
|
||||
|
||||
context.on("click", ".provision_delete_template_button", function(){
|
||||
/* TODO SAVED_TO_IMAGE_ID does not exists anymore and now all the images of the template
|
||||
are cloned instead of only the main disk, therefore all the images should be deleted now.
|
||||
Probably this could be done in the core
|
||||
|
||||
var button = $(this);
|
||||
button.attr("disabled", "disabled");
|
||||
|
||||
var template_id = $(this).attr("template_id");
|
||||
var image_id = $(this).attr("image_id");
|
||||
|
||||
OpenNebula.Image.del({
|
||||
OpenNebula.Template.delete_from_provision({
|
||||
timeout: true,
|
||||
data : {
|
||||
id : image_id
|
||||
id : template_id
|
||||
},
|
||||
success: function (){
|
||||
OpenNebula.Template.del({
|
||||
timeout: true,
|
||||
data : {
|
||||
id : template_id
|
||||
},
|
||||
success: function (){
|
||||
$(".provision_templates_list_refresh_button", context).trigger("click");
|
||||
},
|
||||
error: function (request,error_json, container) {
|
||||
Notifier.onError(request, error_json, container);
|
||||
}
|
||||
})
|
||||
$(".provision_templates_list_refresh_button", context).trigger("click");
|
||||
},
|
||||
error: function (request,error_json, container) {
|
||||
if (error_json.error.http_status=="404") {
|
||||
OpenNebula.Template.del({
|
||||
timeout: true,
|
||||
data : {
|
||||
id : template_id
|
||||
},
|
||||
success: function (){
|
||||
$(".provision_templates_list_refresh_button", context).trigger("click");
|
||||
},
|
||||
error: function (request,error_json, container) {
|
||||
Notifier.onError(request, error_json, container);
|
||||
$(".provision_templates_list_refresh_button", context).trigger("click");
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Notifier.onError(request, error_json, container);
|
||||
}
|
||||
Notifier.onError(request, error_json, container);
|
||||
$(".provision_templates_list_refresh_button", context).trigger("click");
|
||||
}
|
||||
})*/
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@ -301,7 +270,6 @@ define(function(require) {
|
||||
context.on("click", ".provision_confirm_chmod_template_button", function(){
|
||||
var ul_context = $(this).parents(".provision-pricing-table");
|
||||
var template_id = ul_context.attr("opennebula_id");
|
||||
var image_id = ul_context.attr("saved_to_image_id");
|
||||
var template_name = $(".provision-title", ul_context).text();
|
||||
|
||||
$(".provision_confirm_delete_template_div", context).html(
|
||||
@ -315,7 +283,7 @@ define(function(require) {
|
||||
'</span>'+
|
||||
'</div>'+
|
||||
'<div class="large-4 columns">'+
|
||||
'<a href"#" class="provision_chmod_template_button success button large-12 radius right" style="margin-right: 15px" image_id="'+image_id+'" template_id="'+template_id+'">'+Locale.tr("Share template")+'</a>'+
|
||||
'<a href"#" class="provision_chmod_template_button success button large-12 radius right" style="margin-right: 15px" template_id="'+template_id+'">'+Locale.tr("Share template")+'</a>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'<a href="#" class="close">×</a>'+
|
||||
@ -323,16 +291,13 @@ define(function(require) {
|
||||
});
|
||||
|
||||
context.on("click", ".provision_chmod_template_button", function(){
|
||||
/* TODO SAVED_TO_IMAGE_ID does not exists anymore and now all the images of the template
|
||||
are cloned instead of only the main disk, therefore all the images should be chmod now.
|
||||
Probably this could be done in the core
|
||||
|
||||
var button = $(this);
|
||||
button.attr("disabled", "disabled");
|
||||
|
||||
var template_id = $(this).attr("template_id");
|
||||
var image_id = $(this).attr("image_id");
|
||||
|
||||
OpenNebula.Template.chmod({
|
||||
OpenNebula.Template.chmod_from_provision({
|
||||
timeout: true,
|
||||
data : {
|
||||
id : template_id,
|
||||
@ -340,26 +305,14 @@ define(function(require) {
|
||||
},
|
||||
success: function (){
|
||||
$(".provision_templates_list_refresh_button", context).trigger("click");
|
||||
|
||||
OpenNebula.Image.chmod({
|
||||
timeout: true,
|
||||
data : {
|
||||
id : image_id,
|
||||
extra_param: {'group_u': 1}
|
||||
},
|
||||
success: function (){
|
||||
},
|
||||
error: Notifier.onError
|
||||
})
|
||||
},
|
||||
error: Notifier.onError
|
||||
})*/
|
||||
})
|
||||
});
|
||||
|
||||
context.on("click", ".provision_confirm_unshare_template_button", function(){
|
||||
var ul_context = $(this).parents(".provision-pricing-table");
|
||||
var template_id = ul_context.attr("opennebula_id");
|
||||
var image_id = ul_context.attr("saved_to_image_id");
|
||||
var template_name = $(".provision-title", ul_context).first().text();
|
||||
|
||||
$(".provision_confirm_delete_template_div", context).html(
|
||||
@ -373,7 +326,7 @@ define(function(require) {
|
||||
'</span>'+
|
||||
'</div>'+
|
||||
'<div class="large-4 columns">'+
|
||||
'<a href"#" class="provision_unshare_template_button success button large-12 radius right" style="margin-right: 15px" image_id="'+image_id+'" template_id="'+template_id+'">'+Locale.tr("Unshare template")+'</a>'+
|
||||
'<a href"#" class="provision_unshare_template_button success button large-12 radius right" style="margin-right: 15px" template_id="'+template_id+'">'+Locale.tr("Unshare template")+'</a>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'<a href="#" class="close">×</a>'+
|
||||
@ -385,9 +338,8 @@ define(function(require) {
|
||||
button.attr("disabled", "disabled");
|
||||
|
||||
var template_id = $(this).attr("template_id");
|
||||
var image_id = $(this).attr("image_id");
|
||||
|
||||
OpenNebula.Template.chmod({
|
||||
OpenNebula.Template.chmod_from_provision({
|
||||
timeout: true,
|
||||
data : {
|
||||
id : template_id,
|
||||
@ -395,17 +347,6 @@ define(function(require) {
|
||||
},
|
||||
success: function (){
|
||||
$(".provision_templates_list_refresh_button", context).trigger("click");
|
||||
|
||||
OpenNebula.Image.chmod({
|
||||
timeout: true,
|
||||
data : {
|
||||
id : image_id,
|
||||
extra_param: {'group_u': 0}
|
||||
},
|
||||
success: function (){
|
||||
},
|
||||
error: Notifier.onError
|
||||
})
|
||||
},
|
||||
error: Notifier.onError
|
||||
})
|
||||
|
@ -448,7 +448,7 @@ define(function(require) {
|
||||
'<span style="font-size: 14px; line-height: 20px">'+
|
||||
Locale.tr("This Virtual Machine will be saved in a new Template. Only the main disk will be preserved!")+
|
||||
'<br>'+
|
||||
Locale.tr("You can then create a new Virtual Machine using this Template")+
|
||||
Locale.tr("You can then create a new Virtual Machine using this Template.")+
|
||||
'</span>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
@ -483,6 +483,7 @@ define(function(require) {
|
||||
name : template_name
|
||||
}
|
||||
},
|
||||
timeout: false,
|
||||
success: function(request, response){
|
||||
OpenNebula.Action.clear_cache("VMTEMPLATE");
|
||||
Notifier.notifyMessage(Locale.tr("VM Template") + ' ' + request.request.data[0][1].name + ' ' + Locale.tr("saved successfully"))
|
||||
@ -490,7 +491,13 @@ define(function(require) {
|
||||
button.removeAttr("disabled");
|
||||
},
|
||||
error: function(request, response){
|
||||
Notifier.onError(request, response);
|
||||
if(response.error.http_status == 0){ // Failed due to cloning template taking too long
|
||||
OpenNebula.Action.clear_cache("VMTEMPLATE");
|
||||
update_provision_vm_info(vm_id, context);
|
||||
Notifier.notifyMessage(Locale.tr("VM cloning in the background. The Template will appear as soon as it is ready, and the VM unlocked."));
|
||||
} else {
|
||||
Notifier.onError(request, response);
|
||||
}
|
||||
button.removeAttr("disabled");
|
||||
}
|
||||
})
|
||||
|
@ -38,11 +38,17 @@ define(function(require) {
|
||||
//cpu_slider.attr('data-options', 'start: 0; end: 1600; step: 50;');
|
||||
|
||||
cpu_slider.on('change.fndtn.slider', function(){
|
||||
cpu_input.val($(this).attr('data-slider') / 100);
|
||||
if ($(this).attr('data-slider') >= 0) {
|
||||
cpu_input.val($(this).attr('data-slider') / 100);
|
||||
}
|
||||
});
|
||||
|
||||
cpu_input.on('change', function() {
|
||||
cpu_slider.foundation('slider', 'set_value', this.value * 100);
|
||||
if (this.value && this.value >= 0) {
|
||||
cpu_slider.foundation('slider', 'set_value', this.value * 100);
|
||||
} else {
|
||||
cpu_slider.foundation('slider', 'set_value', -1);
|
||||
}
|
||||
});
|
||||
|
||||
cpu_slider.foundation('slider', 'set_value', 100);
|
||||
@ -64,18 +70,30 @@ define(function(require) {
|
||||
}
|
||||
|
||||
memory_input.on('change', function() {
|
||||
$("#memory_slider", context).foundation('slider', 'set_value', this.value * 100);
|
||||
update_final_memory_input();
|
||||
if (this.value && this.value >= 0) {
|
||||
$("#memory_slider", context).foundation('slider', 'set_value', this.value * 100);
|
||||
update_final_memory_input();
|
||||
} else {
|
||||
$("#memory_slider", context).foundation('slider', 'set_value', -1);
|
||||
final_memory_input.val("");
|
||||
}
|
||||
});
|
||||
|
||||
final_memory_input.on('change', function() {
|
||||
$("#memory_slider", context).foundation('slider', 'set_value', this.value * 100);
|
||||
memory_input.val(Math.floor(this.value));
|
||||
if (this.value && this.value >= 0) {
|
||||
$("#memory_slider", context).foundation('slider', 'set_value', this.value * 100);
|
||||
memory_input.val(Math.floor(this.value));
|
||||
} else {
|
||||
$("#memory_slider", context).foundation('slider', 'set_value', -1);
|
||||
memory_input.val("");
|
||||
}
|
||||
});
|
||||
|
||||
$("#memory_slider", context).on('change.fndtn.slider', function() {
|
||||
memory_input.val($(this).attr('data-slider') / 100);
|
||||
update_final_memory_input();
|
||||
if ($(this).attr('data-slider') >= 0) {
|
||||
memory_input.val($(this).attr('data-slider') / 100);
|
||||
update_final_memory_input();
|
||||
}
|
||||
});
|
||||
|
||||
memory_unit.on('change', function() {
|
||||
@ -110,8 +128,10 @@ define(function(require) {
|
||||
memory_input.val(new_val);
|
||||
$("#memory_slider", context).foundation('slider', 'set_value', new_val * 100);
|
||||
$("#memory_slider", context).on('change.fndtn.slider', function() {
|
||||
memory_input.val($(this).attr('data-slider') / 100);
|
||||
update_final_memory_input();
|
||||
if ($(this).attr('data-slider') >= 0) {
|
||||
memory_input.val($(this).attr('data-slider') / 100);
|
||||
update_final_memory_input();
|
||||
}
|
||||
});
|
||||
|
||||
update_final_memory_input();
|
||||
@ -127,13 +147,18 @@ define(function(require) {
|
||||
var vcpu_slider = $("#vcpu_slider", context)
|
||||
|
||||
//vcpu_slider.attr('data-options', 'start: 0; end: 1600; step: 50;');
|
||||
|
||||
vcpu_slider.on('change.fndtn.slider', function(){
|
||||
vcpu_input.val($(this).attr('data-slider') / 100);
|
||||
if ($(this).attr('data-slider') > 0) {
|
||||
vcpu_input.val($(this).attr('data-slider') / 100);
|
||||
}
|
||||
});
|
||||
|
||||
vcpu_input.on('change', function() {
|
||||
vcpu_slider.foundation('slider', 'set_value', this.value * 100);
|
||||
if (this.value && this.value > 0) {
|
||||
vcpu_slider.foundation('slider', 'set_value', this.value * 100);
|
||||
} else {
|
||||
vcpu_slider.foundation('slider', 'set_value', -1);
|
||||
}
|
||||
});
|
||||
|
||||
vcpu_slider.foundation('slider', 'set_value', 0);
|
||||
|
@ -52,7 +52,7 @@
|
||||
{{{tip (tr "Number of virtual cpus. This value is optional, the default hypervisor behavior is used, usually one virtual CPU.")}}}
|
||||
</label>
|
||||
<div class="large-10 columns">
|
||||
<div id="vcpu_slider" class="range-slider radius" data-slider data-options="start: 0; end: 1600; step: 50;">
|
||||
<div id="vcpu_slider" class="range-slider radius" data-slider data-options="start: 100; end: 1600; step: 100;">
|
||||
<span class="range-slider-handle"></span>
|
||||
<span class="range-slider-active-segment"></span>
|
||||
<input type="hidden">
|
||||
|
@ -53,7 +53,7 @@ define(function(require) {
|
||||
|
||||
function _setup(context) {
|
||||
var that = this;
|
||||
that.diskTab.setup();
|
||||
that.diskTab.setup(context);
|
||||
|
||||
Tips.setup(context);
|
||||
|
||||
|
@ -53,7 +53,7 @@ define(function(require) {
|
||||
|
||||
function _setup(context) {
|
||||
var that = this;
|
||||
that.nicTab.setup();
|
||||
that.nicTab.setup(context);
|
||||
|
||||
Tips.setup(context);
|
||||
|
||||
|
@ -900,7 +900,7 @@ class ExecDriver < VirtualMachineDriver
|
||||
target_index = target.downcase[-1..-1].unpack('c').first - 97
|
||||
|
||||
if @options[:detach_snap]
|
||||
disk = xml_data.elements[target_xpath]
|
||||
disk = xml_data.elements[target_xpath].parent
|
||||
attach = REXML::Element.new('ATTACH')
|
||||
|
||||
attach.add_text('YES')
|
||||
@ -973,7 +973,7 @@ class ExecDriver < VirtualMachineDriver
|
||||
target_index = target.downcase[-1..-1].unpack('c').first - 97
|
||||
|
||||
if @options[:detach_snap]
|
||||
disk = xml_data.elements[target_xpath]
|
||||
disk = xml_data.elements[target_xpath].parent
|
||||
attach = REXML::Element.new('ATTACH')
|
||||
|
||||
attach.add_text('YES')
|
||||
|
Loading…
x
Reference in New Issue
Block a user