mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-16 22:50:10 +03:00
Merge branch 'master' of https://github.com/OpenNebula/one-ee
This commit is contained in:
commit
42fe8540fc
@ -242,7 +242,7 @@ private:
|
||||
* image may need to be set to error state.
|
||||
*/
|
||||
void clean_up_vm(VirtualMachine *vm, bool dispose, int& image_id,
|
||||
int uid, int gid, int req_id);
|
||||
int uid, int gid, int req_id, Template& quota_tmpl);
|
||||
};
|
||||
|
||||
#endif /*LIFE_CYCLE_MANAGER_H_*/
|
||||
|
@ -1539,7 +1539,7 @@ public:
|
||||
* @param vm_quotas The SYSTEM_DISK_SIZE freed by the deleted snapshots
|
||||
* @param ds_quotas The DS SIZE freed from image datastores.
|
||||
*/
|
||||
void delete_non_persistent_disk_snapshots(Template **vm_quotas,
|
||||
void delete_non_persistent_disk_snapshots(Template& vm_quotas,
|
||||
std::vector<Template *>& ds_quotas)
|
||||
{
|
||||
disks.delete_non_persistent_snapshots(vm_quotas, ds_quotas);
|
||||
@ -1630,8 +1630,9 @@ public:
|
||||
|
||||
/**
|
||||
* Deletes all SNAPSHOT attributes
|
||||
* @param snapshots Returns template with deleted snapshots
|
||||
*/
|
||||
void delete_snapshots();
|
||||
void delete_snapshots(Template& snapshots);
|
||||
|
||||
/**
|
||||
* Returns size acquired on system DS by VM snapshots
|
||||
|
@ -777,7 +777,7 @@ public:
|
||||
* @param vm_quotas The SYSTEM_DISK_SIZE freed by the deleted snapshots
|
||||
* @param ds_quotas The DS SIZE freed from image datastores.
|
||||
*/
|
||||
void delete_non_persistent_snapshots(Template **vm_quotas,
|
||||
void delete_non_persistent_snapshots(Template &vm_quotas,
|
||||
std::vector<Template *> &ds_quotas);
|
||||
|
||||
/**
|
||||
|
10
install.sh
10
install.sh
@ -758,6 +758,7 @@ INSTALL_FILES=(
|
||||
INSTALL_GEMS_SHARE_FILES:$SHARE_LOCATION
|
||||
ONETOKEN_SHARE_FILE:$SHARE_LOCATION
|
||||
FOLLOWER_CLEANUP_SHARE_FILE:$SHARE_LOCATION
|
||||
PRE_CLEANUP_SHARE_FILE:$SHARE_LOCATION
|
||||
BACKUP_VMS_SHARE_FILE:$SHARE_LOCATION
|
||||
HOOK_AUTOSTART_FILES:$VAR_LOCATION/remotes/hooks/autostart
|
||||
HOOK_FT_FILES:$VAR_LOCATION/remotes/hooks/ft
|
||||
@ -2364,6 +2365,8 @@ ONETOKEN_SHARE_FILE="share/onetoken/onetoken.sh"
|
||||
|
||||
FOLLOWER_CLEANUP_SHARE_FILE="share/hooks/raft/follower_cleanup"
|
||||
|
||||
PRE_CLEANUP_SHARE_FILE="share/pkgs/services/systemd/pre_cleanup"
|
||||
|
||||
BACKUP_VMS_SHARE_FILE="share/scripts/backup_vms"
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
@ -2899,12 +2902,13 @@ FIREEDGE_SUNSTONE_ETC="src/fireedge/etc/sunstone/sunstone-server.conf \
|
||||
src/fireedge/etc/sunstone/sunstone-views.yaml"
|
||||
|
||||
FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN="src/fireedge/etc/sunstone/admin/vm-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/vm-template-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/marketplace-app-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/host-tab.yaml \
|
||||
src/fireedge/etc/sunstone/admin/vm-template-tab.yaml"
|
||||
src/fireedge/etc/sunstone/admin/host-tab.yaml"
|
||||
|
||||
FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml \
|
||||
src/fireedge/etc/sunstone/user/vm-template-tab.yaml"
|
||||
src/fireedge/etc/sunstone/user/vm-template-tab.yaml \
|
||||
src/fireedge/etc/sunstone/user/marketplace-app-tab.yaml"
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# OneGate files
|
||||
|
@ -71,6 +71,9 @@
|
||||
<xs:element name="VENDOR" type="xs:string"/>
|
||||
<xs:element name="VENDOR_NAME" type="xs:string"/>
|
||||
<xs:element name="VMID" type="xs:integer"/>
|
||||
<xs:element name="UUID" type="xs:string"/>
|
||||
<xs:element name="DEVICE_NAME" type="xs:string"/>
|
||||
<xs:element name="PROFILES" type="xs:string"/>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
247
share/install_gems/Ubuntu2204/Gemfile.lock
Normal file
247
share/install_gems/Ubuntu2204/Gemfile.lock
Normal file
@ -0,0 +1,247 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activesupport (4.2.11.3)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
android_key_attestation (0.3.0)
|
||||
augeas (0.6.4)
|
||||
awrence (1.2.1)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.574.0)
|
||||
aws-sdk-cloudwatch (1.62.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-core (3.130.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-ec2 (1.305.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-kms (1.55.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.113.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure_mgmt_compute (0.22.0)
|
||||
ms_rest_azure (~> 0.12.0)
|
||||
azure_mgmt_monitor (0.19.0)
|
||||
ms_rest_azure (~> 0.12.0)
|
||||
azure_mgmt_network (0.26.1)
|
||||
ms_rest_azure (~> 0.12.0)
|
||||
azure_mgmt_resources (0.18.2)
|
||||
ms_rest_azure (~> 0.12.0)
|
||||
azure_mgmt_storage (0.23.0)
|
||||
ms_rest_azure (~> 0.12.0)
|
||||
bindata (2.4.10)
|
||||
builder (3.2.4)
|
||||
cbor (0.5.9.6)
|
||||
chunky_png (1.4.0)
|
||||
concurrent-ruby (1.1.10)
|
||||
configparser (0.1.7)
|
||||
curb (1.0.0)
|
||||
daemons (1.4.1)
|
||||
dalli (2.7.11)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
eventmachine (1.2.7)
|
||||
faraday (1.10.0)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.3)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
ffi (1.15.5)
|
||||
ffi-rzmq (2.0.7)
|
||||
ffi-rzmq-core (>= 1.0.7)
|
||||
ffi-rzmq-core (1.0.7)
|
||||
ffi
|
||||
git (1.10.2)
|
||||
rchardet (~> 1.8)
|
||||
gnuplot (2.6.2)
|
||||
hashie (5.0.0)
|
||||
highline (1.7.10)
|
||||
http-cookie (1.0.4)
|
||||
domain_name (~> 0.5)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
inflection (1.0.0)
|
||||
ipaddr (1.2.4)
|
||||
ipaddress (0.8.3)
|
||||
jmespath (1.6.1)
|
||||
json (2.6.1)
|
||||
jwt (2.3.0)
|
||||
memcache-client (1.8.5)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.8.0)
|
||||
minitest (5.15.0)
|
||||
ms_rest (0.7.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
faraday (>= 0.9, < 2.0.0)
|
||||
timeliness (~> 0.3.10)
|
||||
ms_rest_azure (0.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
faraday (>= 0.9, < 2.0.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
ms_rest (~> 0.7.6)
|
||||
multipart-post (2.1.1)
|
||||
mustermann (1.1.1)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
mysql2 (0.5.3)
|
||||
net-ldap (0.17.0)
|
||||
nokogiri (1.13.3)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
openssl (3.0.0)
|
||||
ipaddr
|
||||
optimist (3.0.1)
|
||||
ox (2.14.11)
|
||||
parse-cron (0.1.4)
|
||||
pg (1.3.5)
|
||||
polyglot (0.3.5)
|
||||
public_suffix (4.0.6)
|
||||
racc (1.6.0)
|
||||
rack (2.2.3)
|
||||
rack-protection (2.2.0)
|
||||
rack
|
||||
rbvmomi (3.0.0)
|
||||
builder (~> 3.2)
|
||||
json (~> 2.3)
|
||||
nokogiri (~> 1.10)
|
||||
optimist (~> 3.0)
|
||||
rchardet (1.8.0)
|
||||
rexml (3.2.5)
|
||||
rotp (6.2.0)
|
||||
rqrcode (2.1.1)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
rqrcode_core (1.2.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
safety_net_attestation (0.4.0)
|
||||
jwt (~> 2.0)
|
||||
sequel (5.55.0)
|
||||
sinatra (2.2.0)
|
||||
mustermann (~> 1.0)
|
||||
rack (~> 2.2)
|
||||
rack-protection (= 2.2.0)
|
||||
tilt (~> 2.0)
|
||||
sqlite3 (1.4.2)
|
||||
thin (1.8.1)
|
||||
daemons (~> 1.0, >= 1.0.9)
|
||||
eventmachine (~> 1.0, >= 1.0.4)
|
||||
rack (>= 1, < 3)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.10)
|
||||
timeliness (0.3.10)
|
||||
treetop (1.6.11)
|
||||
polyglot (~> 0.3)
|
||||
tzinfo (1.2.9)
|
||||
thread_safe (~> 0.1)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.1)
|
||||
uuidtools (2.2.0)
|
||||
vsphere-automation-cis (0.4.7)
|
||||
vsphere-automation-runtime (~> 0.4.6)
|
||||
vsphere-automation-runtime (0.4.7)
|
||||
vsphere-automation-vcenter (0.4.7)
|
||||
vsphere-automation-cis (~> 0.4.6)
|
||||
vsphere-automation-runtime (~> 0.4.6)
|
||||
webrick (1.7.0)
|
||||
xmlrpc (0.3.2)
|
||||
webrick
|
||||
zendesk_api (1.35.0)
|
||||
faraday (>= 0.9.0, < 2.0.0)
|
||||
hashie (>= 3.5.2, < 6.0.0)
|
||||
inflection
|
||||
mini_mime
|
||||
multipart-post (~> 2.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
activesupport (~> 4.2)
|
||||
addressable
|
||||
augeas (~> 0.6)
|
||||
aws-sdk-cloudwatch
|
||||
aws-sdk-ec2 (>= 1.151)
|
||||
aws-sdk-s3
|
||||
azure_mgmt_compute
|
||||
azure_mgmt_monitor
|
||||
azure_mgmt_network
|
||||
azure_mgmt_resources
|
||||
azure_mgmt_storage
|
||||
configparser
|
||||
curb
|
||||
dalli (< 3.0)
|
||||
faraday_middleware (~> 1.2.0)
|
||||
ffi-rzmq (~> 2.0.7)
|
||||
git (~> 1.5)
|
||||
gnuplot
|
||||
highline (~> 1.7)
|
||||
i18n (~> 0.9)
|
||||
ipaddress (~> 0.8.3)
|
||||
json (>= 2.0)
|
||||
memcache-client
|
||||
minitest
|
||||
mysql2
|
||||
net-ldap
|
||||
nokogiri
|
||||
ox
|
||||
parse-cron
|
||||
pg
|
||||
public_suffix
|
||||
rack
|
||||
rbvmomi (~> 3.0.0)
|
||||
rotp
|
||||
rqrcode
|
||||
sequel
|
||||
sinatra
|
||||
sqlite3
|
||||
thin
|
||||
treetop (>= 1.6.3)
|
||||
uuidtools
|
||||
vsphere-automation-cis (~> 0.4.6)
|
||||
vsphere-automation-vcenter (~> 0.4.6)
|
||||
webauthn
|
||||
xmlrpc
|
||||
zendesk_api
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.7.0p0
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.3
|
@ -680,9 +680,6 @@ Style/RedundantBegin:
|
||||
Style/ExponentialNotation:
|
||||
Enabled: true
|
||||
|
||||
Style/SlicingWithRange:
|
||||
Enabled: true
|
||||
|
||||
Style/AccessorGrouping:
|
||||
Enabled: False
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
- Name: "{{ monitor_interface }}"
|
||||
- Network:
|
||||
- DHCP: "no"
|
||||
- Address: "{{ (eth1_ip + '/' + ansible_default_ipv4.netmask) | ipaddr('host/prefix') }}"
|
||||
- Address: "{{ (eth1_ip + '/' + ansible_default_ipv4.netmask) | ansible.utils.ipaddr('host/prefix') }}"
|
||||
when: setup_eth1 | default('false') | bool
|
||||
|
||||
- hosts:
|
||||
|
@ -1,2 +1,5 @@
|
||||
# enables and configure systemd-networkd
|
||||
- src: stackhpc.systemd_networkd
|
||||
roles:
|
||||
- stackhpc.systemd_networkd
|
||||
|
||||
collections:
|
||||
- ansible.utils
|
||||
|
@ -15,6 +15,7 @@ User=oneadmin
|
||||
Environment="PATH=/usr/lib/one/sh/override:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
EnvironmentFile=-/var/run/one/ssh-agent.env
|
||||
ExecStartPre=-/usr/sbin/logrotate -f /etc/logrotate.d/opennebula -s /var/lib/one/.logrotate.status
|
||||
ExecStartPre=/usr/share/one/pre_cleanup
|
||||
ExecStart=/usr/bin/oned -f
|
||||
ExecStopPost=/usr/share/one/follower_cleanup
|
||||
PIDFile=/var/lock/one/one
|
||||
|
5
share/pkgs/services/systemd/pre_cleanup
Executable file
5
share/pkgs/services/systemd/pre_cleanup
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ ! -f /var/run/one/oned.pid && -f /var/lock/one/one ]]; then
|
||||
rm /var/lock/one/one
|
||||
fi
|
@ -520,59 +520,4 @@ class OneVcenterHelper < OpenNebulaHelper::OneHelper
|
||||
opts
|
||||
end
|
||||
|
||||
def clear_tags(vmid)
|
||||
client = Client.new
|
||||
|
||||
vm_pool = VirtualMachinePool.new(client, -1)
|
||||
host_pool = HostPool.new(client)
|
||||
deploy_id = -1
|
||||
host_id = -1
|
||||
hostname = ''
|
||||
|
||||
rc = vm_pool.info
|
||||
raise rc.message if OpenNebula.is_error?(rc)
|
||||
|
||||
rc = host_pool.info
|
||||
raise rc.message if OpenNebula.is_error?(rc)
|
||||
|
||||
vm_pool.each do |vm|
|
||||
next if vm.id.to_s != vmid
|
||||
|
||||
deploy_id = vm.deploy_id
|
||||
vm_history = vm.to_hash['VM']['HISTORY_RECORDS']['HISTORY']
|
||||
hostname = vm_history['HOSTNAME']
|
||||
break
|
||||
end
|
||||
|
||||
host_pool.each do |host|
|
||||
if host.name == hostname
|
||||
host_id = host.id
|
||||
end
|
||||
end
|
||||
|
||||
vi_client = VCenterDriver::VIClient.new_from_host(host_id)
|
||||
vm = VCenterDriver::VirtualMachine
|
||||
.new(vi_client, deploy_id, vmid)
|
||||
|
||||
keys_to_remove = []
|
||||
vm['config.extraConfig'].each do |extraconfig|
|
||||
next unless extraconfig.key.include?('opennebula.disk') ||
|
||||
extraconfig.key.include?('opennebula.vm') ||
|
||||
extraconfig.key.downcase.include?('remotedisplay')
|
||||
|
||||
keys_to_remove << extraconfig.key
|
||||
end
|
||||
|
||||
[vm, keys_to_remove]
|
||||
end
|
||||
|
||||
def remove_keys(vm, keys_to_remove)
|
||||
spec_hash = keys_to_remove.map {|key| { :key => key, :value => '' } }
|
||||
|
||||
spec = RbVmomi::VIM.VirtualMachineConfigSpec(
|
||||
:extraConfig => spec_hash
|
||||
)
|
||||
vm.item.ReconfigVM_Task(:spec => spec).wait_for_completion
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -297,7 +297,7 @@ class OneVMHelper < OpenNebulaHelper::OneHelper
|
||||
vm.info
|
||||
ids = vm.retrieve_elements("/VM/SNAPSHOTS/SNAPSHOT[NAME='#{id}']/ID")
|
||||
|
||||
return [-1, "#{id} not found or duplicated"] \
|
||||
return [-1, "Snapshot #{id} not found or duplicated"] \
|
||||
if ids.nil? || ids.size > 1
|
||||
|
||||
[0, ids[0].to_i]
|
||||
|
@ -257,7 +257,8 @@ CommandParser::CmdParser.new(ARGV) do
|
||||
:options => [OneProvisionHelper::MODES,
|
||||
OneProvisionHelper::AMOUNT,
|
||||
OneProvisionHelper::HOSTNAMES,
|
||||
OneProvisionHelper::HOST_PARAMS] do
|
||||
OneProvisionHelper::HOST_PARAMS] +
|
||||
[OpenNebulaHelper::FORMAT] do
|
||||
helper.add_hosts(args[0], options)
|
||||
end
|
||||
|
||||
@ -270,7 +271,8 @@ CommandParser::CmdParser.new(ARGV) do
|
||||
command [:host, :delete],
|
||||
host_delete_desc,
|
||||
[:range, :hostid_list],
|
||||
:options => [OneProvisionHelper::MODES] do
|
||||
:options => [OneProvisionHelper::MODES] +
|
||||
[OpenNebulaHelper::FORMAT] do
|
||||
operation = { :operation => 'delete', :message => 'deleted' }
|
||||
|
||||
helper.resources_operation(args, operation, options, 'HOST')
|
||||
@ -285,7 +287,8 @@ CommandParser::CmdParser.new(ARGV) do
|
||||
command [:host, :configure],
|
||||
host_configure_desc,
|
||||
[:range, :hostid_list],
|
||||
:options => [OneProvisionHelper::MODES] do
|
||||
:options => [OneProvisionHelper::MODES] +
|
||||
[OpenNebulaHelper::FORMAT] do
|
||||
operation = { :operation => 'configure', :message => 'enabled' }
|
||||
|
||||
helper.resources_operation(args, operation, options, 'HOST')
|
||||
|
@ -409,7 +409,42 @@ CommandParser::CmdParser.new(ARGV) do
|
||||
|
||||
begin
|
||||
print '.'
|
||||
vm, keys = helper.clear_tags(vmid)
|
||||
|
||||
client = Client.new
|
||||
|
||||
vm_pool = VirtualMachinePool.new(client, -1)
|
||||
host_pool = HostPool.new(client)
|
||||
deploy_id = -1
|
||||
host_id = -1
|
||||
hostname = ''
|
||||
|
||||
rc = vm_pool.info
|
||||
raise rc.message if OpenNebula.is_error?(rc)
|
||||
|
||||
rc = host_pool.info
|
||||
raise rc.message if OpenNebula.is_error?(rc)
|
||||
|
||||
vm_pool.each do |vm|
|
||||
next if vm.id.to_s != vmid
|
||||
|
||||
deploy_id = vm.deploy_id
|
||||
vm_history = vm.to_hash['VM']['HISTORY_RECORDS']['HISTORY']
|
||||
hostname = vm_history['HOSTNAME']
|
||||
break
|
||||
end
|
||||
|
||||
host_pool.each do |host|
|
||||
if host.name == hostname
|
||||
host_id = host.id
|
||||
end
|
||||
end
|
||||
|
||||
vi_client = VCenterDriver::VIClient.new_from_host(host_id)
|
||||
vm = VCenterDriver::VirtualMachine
|
||||
.new(vi_client, deploy_id, vmid)
|
||||
|
||||
keys = vm.extra_config_keys
|
||||
|
||||
print '.'
|
||||
|
||||
if keys.empty?
|
||||
@ -422,7 +457,7 @@ CommandParser::CmdParser.new(ARGV) do
|
||||
puts 'The following keys will be removed:'
|
||||
keys.each {|key| puts "\t- #{key}" }
|
||||
|
||||
helper.remove_keys(vm, keys)
|
||||
vm.clear_tags
|
||||
rescue StandardError => e
|
||||
STDERR.puts "Couldn't clear VM tags. Reason: #{e.message}"
|
||||
exit 1
|
||||
|
@ -472,7 +472,13 @@ CommandParser::CmdParser.new(ARGV) do
|
||||
verbose = "disk #{disk_id} prepared to be saved in " \
|
||||
"the image #{image_name}"
|
||||
else
|
||||
snapshot_id = snapshot_id.to_i
|
||||
err, snapshot_id = helper.retrieve_disk_snapshot_id(args[0],
|
||||
snapshot_id)
|
||||
|
||||
if err == -1
|
||||
STDERR.puts snapshot_id
|
||||
exit(-1)
|
||||
end
|
||||
|
||||
verbose = "disk #{disk_id} snapshot #{snapshot_id} prepared to " \
|
||||
"be saved in the image #{image_name}"
|
||||
|
@ -30,16 +30,27 @@ namespace ssl_util
|
||||
{
|
||||
const int max_size = 3 * in.length()/4 + 1;
|
||||
auto output = new unsigned char[max_size];
|
||||
|
||||
|
||||
int size = EVP_DecodeBlock(output, reinterpret_cast<const unsigned char *>(in.c_str()), in.length());
|
||||
|
||||
|
||||
if (size <= 0)
|
||||
{
|
||||
out.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
while (output[size-1] == '\0') { --size; } // Trim trailling 0
|
||||
/* Subtract padding bytes from |size|. Any more than 2 is malformed. */
|
||||
size_t inlen = in.length();
|
||||
int i = 0;
|
||||
while (in[--inlen] == '=')
|
||||
{
|
||||
--size;
|
||||
if (++i > 2)
|
||||
{
|
||||
out.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
out.assign(reinterpret_cast<char*>(output), size);
|
||||
|
||||
@ -53,7 +64,7 @@ namespace ssl_util
|
||||
{
|
||||
const int max_size = 4*((in.length()+2)/3) + 1;
|
||||
auto output = new char[max_size];
|
||||
|
||||
|
||||
const int size = EVP_EncodeBlock(reinterpret_cast<unsigned char *>(output),
|
||||
reinterpret_cast<const unsigned char *>(in.c_str()), in.length());
|
||||
|
||||
@ -66,7 +77,7 @@ namespace ssl_util
|
||||
out.assign(output, size);
|
||||
|
||||
delete[] output;
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -566,11 +566,11 @@ int Datastore::set_ds_disk_type(string& s_dt, string& error)
|
||||
//Valid disk types for System DS
|
||||
case Image::FILE:
|
||||
case Image::RBD:
|
||||
case Image::BLOCK:
|
||||
break;
|
||||
|
||||
case Image::GLUSTER:
|
||||
case Image::SHEEPDOG:
|
||||
case Image::BLOCK:
|
||||
case Image::ISCSI:
|
||||
case Image::CD_ROM:
|
||||
case Image::RBD_CDROM:
|
||||
|
@ -186,7 +186,7 @@ function create_signature {
|
||||
serviceKey=$(hmac_sha256 hexkey:"${regionKey}" "s3")
|
||||
signingKey=$(hmac_sha256 hexkey:"${serviceKey}" "aws4_request")
|
||||
|
||||
printf "${stringToSign}" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"${signingKey}" | sed 's/(stdin)= //'
|
||||
printf "${stringToSign}" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"${signingKey}" | sed 's/.*(stdin)= //'
|
||||
}
|
||||
|
||||
function s3_curl_args
|
||||
|
@ -320,6 +320,13 @@ void DispatchManager::free_vm_resources(unique_ptr<VirtualMachine> vm,
|
||||
|
||||
vm->set_exit_time(time(0));
|
||||
|
||||
if (vm->hasHistory() && vm->get_etime() == 0)
|
||||
{
|
||||
vm->set_etime(time(0));
|
||||
|
||||
vmpool->update_history(vm.get());
|
||||
}
|
||||
|
||||
VectorAttribute * graphics = vm->get_template_attribute("GRAPHICS");
|
||||
|
||||
if ( graphics != nullptr && graphics->vector_value("PORT", port) == 0
|
||||
@ -1161,7 +1168,7 @@ int DispatchManager::delete_recreate(unique_ptr<VirtualMachine> vm,
|
||||
|
||||
int rc = 0;
|
||||
|
||||
Template * vm_quotas_snp = nullptr;
|
||||
Template vm_quotas_snp;
|
||||
|
||||
VirtualMachineTemplate quota_tmpl;
|
||||
bool do_quotas = false;
|
||||
@ -1195,7 +1202,7 @@ int DispatchManager::delete_recreate(unique_ptr<VirtualMachine> vm,
|
||||
vm_uid = vm->get_uid();
|
||||
vm_gid = vm->get_gid();
|
||||
|
||||
vm->delete_non_persistent_disk_snapshots(&vm_quotas_snp,
|
||||
vm->delete_non_persistent_disk_snapshots(vm_quotas_snp,
|
||||
ds_quotas_snp);
|
||||
|
||||
do_quotas = true;
|
||||
@ -1239,11 +1246,9 @@ int DispatchManager::delete_recreate(unique_ptr<VirtualMachine> vm,
|
||||
Quotas::ds_del_recreate(vm_uid, vm_gid, ds_quotas_snp);
|
||||
}
|
||||
|
||||
if ( vm_quotas_snp != nullptr )
|
||||
if ( !vm_quotas_snp.empty() )
|
||||
{
|
||||
Quotas::vm_del(vm_uid, vm_gid, vm_quotas_snp);
|
||||
|
||||
delete vm_quotas_snp;
|
||||
Quotas::vm_del(vm_uid, vm_gid, &vm_quotas_snp);
|
||||
}
|
||||
|
||||
if ( do_quotas )
|
||||
@ -2011,7 +2016,7 @@ int DispatchManager::disk_snapshot_revert(int vid, int did, int snap_id,
|
||||
if (vm->set_snapshot_disk(did, snap_id) == -1)
|
||||
{
|
||||
oss << "Disk id (" << did << ") or snapshot id ("
|
||||
<< snap_id << ") is not invalid.";
|
||||
<< snap_id << ") is not valid.";
|
||||
|
||||
error_str = oss.str();
|
||||
|
||||
|
@ -32,6 +32,7 @@ actions:
|
||||
enable: true
|
||||
disable: true
|
||||
delete: true
|
||||
edit_labels: true
|
||||
|
||||
# Filters - List of criteria to filter the resources
|
||||
|
||||
|
76
src/fireedge/etc/sunstone/user/marketplace-app-tab.yaml
Normal file
76
src/fireedge/etc/sunstone/user/marketplace-app-tab.yaml
Normal file
@ -0,0 +1,76 @@
|
||||
# -------------------------------------------------------------------------- #
|
||||
# Copyright 2002-2022, OpenNebula Project, OpenNebula Systems #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
|
||||
# not use this file except in compliance with the License. You may obtain #
|
||||
# a copy of the License at #
|
||||
# #
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
#--------------------------------------------------------------------------- #
|
||||
|
||||
---
|
||||
# This file describes the information and actions available in the App tab
|
||||
|
||||
# Resource
|
||||
|
||||
resource_name: "MARKETPLACE-APP"
|
||||
|
||||
# Actions - Which buttons are visible to operate over the resources
|
||||
|
||||
actions:
|
||||
create_dialog: true
|
||||
export: true
|
||||
download: true
|
||||
chown: true
|
||||
chgrp: true
|
||||
enable: true
|
||||
disable: true
|
||||
delete: true
|
||||
edit_labels: true
|
||||
|
||||
# Filters - List of criteria to filter the resources
|
||||
|
||||
filters:
|
||||
label: true
|
||||
owner: true
|
||||
group: true
|
||||
state: true
|
||||
type: true
|
||||
marketplace: true
|
||||
zone: true
|
||||
|
||||
# Info Tabs - Which info tabs are used to show extended information
|
||||
|
||||
info-tabs:
|
||||
|
||||
info:
|
||||
enabled: true
|
||||
information_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
rename: true
|
||||
permissions_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
chmod: true
|
||||
ownership_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
chown: true
|
||||
chgrp: true
|
||||
attributes_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
copy: true
|
||||
add: true
|
||||
edit: true
|
||||
delete: true
|
||||
|
||||
template:
|
||||
enabled: true
|
@ -80,6 +80,7 @@ const MarketplaceAppCard = memo(
|
||||
() =>
|
||||
getUniqueLabels(LABELS).map((label) => ({
|
||||
text: label,
|
||||
dataCy: `label-${label}`,
|
||||
stateColor: getColorFromString(label),
|
||||
onClick: onClickLabel,
|
||||
onDelete: enableEditLabels && onDeleteLabel,
|
||||
|
@ -41,6 +41,7 @@ const ProvisionCard = memo(
|
||||
isProvider,
|
||||
actions,
|
||||
deleteAction,
|
||||
configureAction,
|
||||
}) => {
|
||||
const {
|
||||
ID,
|
||||
@ -64,11 +65,12 @@ const ProvisionCard = memo(
|
||||
return (
|
||||
<SelectCard
|
||||
action={
|
||||
(actions?.length > 0 || deleteAction) && (
|
||||
(actions?.length > 0 || deleteAction || configureAction) && (
|
||||
<>
|
||||
{actions?.map((action) => (
|
||||
<Action key={action?.cy} {...action} />
|
||||
))}
|
||||
{configureAction && <ButtonToTriggerForm {...configureAction} />}
|
||||
{deleteAction && <ButtonToTriggerForm {...deleteAction} />}
|
||||
</>
|
||||
)
|
||||
@ -127,6 +129,7 @@ ProvisionCard.propTypes = {
|
||||
isProvider: PropTypes.bool,
|
||||
image: PropTypes.string,
|
||||
deleteAction: PropTypes.object,
|
||||
configureAction: PropTypes.object,
|
||||
actions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
handleClick: PropTypes.func.isRequired,
|
||||
@ -143,6 +146,7 @@ ProvisionCard.defaultProps = {
|
||||
isSelected: undefined,
|
||||
image: undefined,
|
||||
deleteAction: undefined,
|
||||
confifureAction: undefined,
|
||||
value: {},
|
||||
}
|
||||
|
||||
|
@ -64,10 +64,15 @@ export const concatNewMessageToLog = (log, message = {}) => {
|
||||
|
||||
const { data, command, commandId } = message
|
||||
|
||||
return {
|
||||
...log,
|
||||
[command]: {
|
||||
if (log?.[command]?.[commandId] !== undefined) {
|
||||
log[command][commandId]?.push(data)
|
||||
} else if (log?.[command] !== undefined) {
|
||||
log[command][commandId] = [data]
|
||||
} else {
|
||||
log[command] = {
|
||||
[commandId]: [...(log?.[command]?.[commandId] ?? []), data],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return { ...log }
|
||||
}
|
||||
|
@ -21,22 +21,16 @@ import AdornmentWithTooltip from 'client/components/FormControl/Tooltip'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
|
||||
const StyledLegend = styled((props) => (
|
||||
<Typography variant="subtitle1" component="legend" {...props} />
|
||||
))(
|
||||
({ theme }) => ({
|
||||
padding: '0em 1em 0.2em 0.5em',
|
||||
borderBottom: `2px solid ${theme.palette.secondary.main}`,
|
||||
<Typography variant="underline" component="legend" {...props} />
|
||||
))(({ ownerState }) => ({
|
||||
...(ownerState.tooltip && {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
({ ownerState }) => ({
|
||||
...(ownerState.tooltip && {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
...(!ownerState.disableGutters && {
|
||||
marginBottom: '1em',
|
||||
}),
|
||||
})
|
||||
)
|
||||
...(!ownerState.disableGutters && {
|
||||
marginBottom: '1em',
|
||||
}),
|
||||
}))
|
||||
|
||||
const Legend = memo(
|
||||
({ 'data-cy': dataCy, title, tooltip, disableGutters }) => (
|
||||
|
@ -0,0 +1,24 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { createForm } from 'client/utils'
|
||||
import {
|
||||
SCHEMA,
|
||||
FIELDS,
|
||||
} from 'client/components/Forms/Provision/ConfigureForm/schema'
|
||||
|
||||
const ConfigureForm = createForm(SCHEMA, FIELDS)
|
||||
|
||||
export default ConfigureForm
|
@ -0,0 +1,32 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { object, boolean } from 'yup'
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
import { getValidationFromFields } from 'client/utils'
|
||||
|
||||
const FORCE = {
|
||||
name: 'force',
|
||||
label: T.Force,
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
tooltip: T.ForceConcept,
|
||||
validation: boolean()
|
||||
.notRequired()
|
||||
.default(() => false),
|
||||
}
|
||||
|
||||
export const FIELDS = [FORCE]
|
||||
|
||||
export const SCHEMA = object(getValidationFromFields(FIELDS))
|
@ -31,4 +31,11 @@ const CreateForm = (configProps) =>
|
||||
const DeleteForm = (configProps) =>
|
||||
AsyncLoadForm({ formPath: 'Provision/DeleteForm' }, configProps)
|
||||
|
||||
export { CreateForm, DeleteForm }
|
||||
/**
|
||||
* @param {ConfigurationProps} configProps - Configuration
|
||||
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
|
||||
*/
|
||||
const ConfigureForm = (configProps) =>
|
||||
AsyncLoadForm({ formPath: 'Provision/ConfigureForm' }, configProps)
|
||||
|
||||
export { CreateForm, DeleteForm, ConfigureForm }
|
||||
|
@ -26,41 +26,39 @@ const callAll =
|
||||
(...args) =>
|
||||
fns.forEach((fn) => fn && fn?.(...args))
|
||||
|
||||
const Chip = styled(Typography)(
|
||||
({ theme: { palette }, state = 'debug', ownerState }) => {
|
||||
const { dark = state } = palette[state] ?? {}
|
||||
const Chip = styled(Typography)(({ theme: { palette }, ownerState }) => {
|
||||
const { dark = ownerState.state } = palette[ownerState.state] ?? {}
|
||||
|
||||
const isWhite = ownerState.forceWhiteColor
|
||||
const bgColor = alpha(dark, palette.mode === SCHEMES.DARK ? 0.5 : 0.2)
|
||||
const color = isWhite ? palette.common.white : palette.text.primary
|
||||
const iconColor = isWhite ? palette.getContrastText(color) : dark
|
||||
const isWhite = ownerState.forceWhiteColor
|
||||
const bgColor = alpha(dark, palette.mode === SCHEMES.DARK ? 0.5 : 0.2)
|
||||
const color = isWhite ? palette.common.white : palette.text.primary
|
||||
const iconColor = isWhite ? palette.getContrastText(color) : dark
|
||||
|
||||
return {
|
||||
color,
|
||||
backgroundColor: bgColor,
|
||||
padding: ownerState.hasIcon ? '0.1rem 0.5rem' : '0.25rem 0.5rem',
|
||||
cursor: 'default',
|
||||
userSelect: 'none',
|
||||
...(ownerState.hasIcon && {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em',
|
||||
'& > .icon': {
|
||||
cursor: 'pointer',
|
||||
color,
|
||||
'&:hover': { color: iconColor },
|
||||
},
|
||||
}),
|
||||
...(ownerState.clickable && {
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
return {
|
||||
color,
|
||||
backgroundColor: bgColor,
|
||||
padding: ownerState.hasIcon ? '0.1rem 0.5rem' : '0.25rem 0.5rem',
|
||||
cursor: 'default',
|
||||
userSelect: 'none',
|
||||
...(ownerState.hasIcon && {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em',
|
||||
'& > .icon': {
|
||||
cursor: 'pointer',
|
||||
'&:hover, &:focus': {
|
||||
backgroundColor: alpha(bgColor, 0.3),
|
||||
},
|
||||
}),
|
||||
}
|
||||
color,
|
||||
'&:hover': { color: iconColor },
|
||||
},
|
||||
}),
|
||||
...(ownerState.clickable && {
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
cursor: 'pointer',
|
||||
'&:hover, &:focus': {
|
||||
backgroundColor: alpha(bgColor, 0.3),
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const StatusChip = memo(
|
||||
({
|
||||
@ -79,6 +77,7 @@ const StatusChip = memo(
|
||||
forceWhiteColor,
|
||||
hasIcon: clipboard || onDelete ? 'true' : undefined,
|
||||
clickable: !!onClick,
|
||||
state: stateColor || 'debug',
|
||||
}
|
||||
|
||||
const handleCopy = useCallback(
|
||||
@ -113,7 +112,6 @@ const StatusChip = memo(
|
||||
variant="overline"
|
||||
lineHeight="normal"
|
||||
borderRadius="0.5em"
|
||||
state={stateColor}
|
||||
ownerState={ownerState}
|
||||
onClick={callAll(handleClick, clipboard && handleCopy)}
|
||||
data-cy={dataCy}
|
||||
|
@ -16,7 +16,7 @@
|
||||
import { ReactElement, Fragment, memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Stack, Button } from '@mui/material'
|
||||
import { Stack } from '@mui/material'
|
||||
import { Filter } from 'iconoir-react'
|
||||
import { UseFiltersInstanceProps, UseFiltersState } from 'react-table'
|
||||
|
||||
@ -33,7 +33,7 @@ import { T } from 'client/constants'
|
||||
const GlobalFilter = memo(
|
||||
(tableProps) => {
|
||||
/** @type {UseFiltersInstanceProps} */
|
||||
const { rows, columns, setAllFilters, state } = tableProps
|
||||
const { rows, columns, state } = tableProps
|
||||
|
||||
/** @type {UseFiltersState} */
|
||||
const { filters } = state
|
||||
@ -57,8 +57,8 @@ const GlobalFilter = memo(
|
||||
<HeaderPopover
|
||||
id="filter-by-button"
|
||||
icon={<Filter />}
|
||||
headerTitle={T.FilterBy}
|
||||
buttonLabel={T.Filter}
|
||||
headerTitle={<Translate word={T.FilterBy} />}
|
||||
buttonLabel={<Translate word={T.Filter} />}
|
||||
buttonProps={{
|
||||
'data-cy': 'filter-by-button',
|
||||
disableElevation: true,
|
||||
@ -69,18 +69,10 @@ const GlobalFilter = memo(
|
||||
popperProps={{ placement: 'bottom-end' }}
|
||||
>
|
||||
{() => (
|
||||
<Stack sx={{ width: { xs: '100%', md: 500 }, p: 2 }}>
|
||||
<Stack sx={{ width: { xs: '100%', md: 500 } }}>
|
||||
{columnsCanFilter.map((column, idx) => (
|
||||
<Fragment key={idx}>{column.render('Filter')}</Fragment>
|
||||
))}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => setAllFilters([])}
|
||||
sx={{ mt: 2, alignSelf: 'flex-end' }}
|
||||
>
|
||||
<Translate word={T.Clear} />
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</HeaderPopover>
|
||||
|
@ -0,0 +1,159 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import CheckIcon from 'iconoir-react/dist/Check'
|
||||
import MinusIcon from 'iconoir-react/dist/Minus'
|
||||
import { styled, debounce, Box, Typography, Autocomplete } from '@mui/material'
|
||||
|
||||
import {
|
||||
PopperComponent,
|
||||
StyledInput,
|
||||
} from 'client/components/Tables/Enhanced/Utils/GlobalLabel/styles'
|
||||
import { StatusCircle } from 'client/components/Status'
|
||||
import { getColorFromString } from 'client/models/Helper'
|
||||
import { Translate, Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const EmptyIcon = styled((props) => <Box component="span" {...props} />)({
|
||||
width: 20,
|
||||
height: 20,
|
||||
})
|
||||
|
||||
const Label = ({ label, indeterminate, selected, ...props }) => (
|
||||
<Box component="li" gap="0.5em" {...props}>
|
||||
{selected ? <CheckIcon /> : indeterminate ? <MinusIcon /> : <EmptyIcon />}
|
||||
<StatusCircle color={getColorFromString(label)} size={18} />
|
||||
<Typography noWrap variant="body2" sx={{ flexGrow: 1 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
|
||||
Label.propTypes = {
|
||||
label: PropTypes.any,
|
||||
indeterminate: PropTypes.bool,
|
||||
selected: PropTypes.bool,
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates labels to the selected rows.
|
||||
*
|
||||
* @param {object} props - Component props
|
||||
* @param {string[]} props.labels - The list of available labels
|
||||
* @param {string<string[]>} props.selectedLabels - The list of selected labels
|
||||
* @param {string[]} props.pendingValue - The current value of the filter
|
||||
* @param {function(any)} props.handleChange - Handle change event
|
||||
* @param {function()} props.handleClose - Handle close event
|
||||
* @returns {ReactElement} Allocator component
|
||||
*/
|
||||
const LabelAllocator = ({
|
||||
labels,
|
||||
selectedLabels,
|
||||
pendingValue,
|
||||
handleChange,
|
||||
handleClose,
|
||||
}) => {
|
||||
const getLabelProps = useCallback(
|
||||
(label) => {
|
||||
const labelProps = { label }
|
||||
|
||||
// labels that exists on every row
|
||||
if (pendingValue[0]?.includes(label))
|
||||
return { ...labelProps, selected: true }
|
||||
|
||||
// labels to remove from every row
|
||||
if (pendingValue[1]?.includes(label)) {
|
||||
return { ...labelProps, selected: false }
|
||||
}
|
||||
|
||||
return selectedLabels.reduce((res, rowLabels) => {
|
||||
const hasLabel = rowLabels.includes(label)
|
||||
const prevSelected = [true, undefined].includes(res.selected)
|
||||
|
||||
hasLabel
|
||||
? (res.indeterminate = !prevSelected)
|
||||
: (res.indeterminate ||= res.selected)
|
||||
|
||||
return { ...res, selected: hasLabel && prevSelected }
|
||||
}, labelProps)
|
||||
},
|
||||
[pendingValue, selectedLabels]
|
||||
)
|
||||
|
||||
const handleLabelChange = useCallback(
|
||||
debounce((event, newValue, reason) => {
|
||||
const changeFn = {
|
||||
selectOption: ([, toRemove = []] = []) => [
|
||||
newValue,
|
||||
toRemove?.filter((label) => !newValue.includes(label)),
|
||||
],
|
||||
removeOption: ([toAdd = [], toRemove = []] = []) => {
|
||||
const prevToAdd = toAdd?.filter((label) => !newValue.includes(label))
|
||||
const newToRemove = [...toRemove, ...prevToAdd]
|
||||
|
||||
return [newValue, [...new Set(newToRemove)]]
|
||||
},
|
||||
}[reason]
|
||||
|
||||
changeFn && handleChange(changeFn)
|
||||
}, 200),
|
||||
[handleChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
open
|
||||
multiple
|
||||
value={pendingValue[0]}
|
||||
onClose={(_, reason) => reason === 'escape' && handleClose()}
|
||||
onChange={handleLabelChange}
|
||||
disableCloseOnSelect
|
||||
PopperComponent={PopperComponent}
|
||||
renderTags={() => null}
|
||||
noOptionsText={<Translate word={T.NoLabels} />}
|
||||
renderOption={(props, label) => (
|
||||
<Label key={label} {...props} {...getLabelProps(label)} />
|
||||
)}
|
||||
isOptionEqualToValue={(option, value) =>
|
||||
Array.isArray(value) ? value.includes(option) : value === option
|
||||
}
|
||||
options={labels}
|
||||
renderInput={(params) => (
|
||||
<StyledInput
|
||||
ref={params.InputProps.ref}
|
||||
inputProps={params.inputProps}
|
||||
autoFocus
|
||||
placeholder={Tr(T.Search)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
LabelAllocator.propTypes = {
|
||||
labels: PropTypes.array,
|
||||
selectedLabels: PropTypes.array,
|
||||
indeterminateLabels: PropTypes.array,
|
||||
pendingValue: PropTypes.array,
|
||||
handleChange: PropTypes.func,
|
||||
handleClose: PropTypes.func,
|
||||
}
|
||||
|
||||
LabelAllocator.displayName = 'LabelAllocator'
|
||||
|
||||
export default LabelAllocator
|
@ -13,144 +13,137 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, SyntheticEvent } from 'react'
|
||||
import { memo, ReactElement, MouseEvent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import CheckIcon from 'iconoir-react/dist/Check'
|
||||
import CancelIcon from 'iconoir-react/dist/Cancel'
|
||||
import { styled, Box, InputBase, Typography } from '@mui/material'
|
||||
import Autocomplete, {
|
||||
autocompleteClasses,
|
||||
AutocompleteChangeDetails,
|
||||
AutocompleteChangeReason,
|
||||
AutocompleteCloseReason,
|
||||
} from '@mui/material/Autocomplete'
|
||||
import LockIcon from 'iconoir-react/dist/Lock'
|
||||
import { Box, Typography, Autocomplete } from '@mui/material'
|
||||
|
||||
import { useAddLabelMutation } from 'client/features/OneApi/auth'
|
||||
|
||||
import {
|
||||
PopperComponent,
|
||||
StyledInput,
|
||||
} from 'client/components/Tables/Enhanced/Utils/GlobalLabel/styles'
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
import { StatusCircle } from 'client/components/Status'
|
||||
import { getColorFromString } from 'client/models/Helper'
|
||||
import { Translate, Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const StyledInput = styled(InputBase)(
|
||||
({ theme: { shape, palette, transitions } }) => ({
|
||||
padding: 10,
|
||||
width: '100%',
|
||||
'& input': {
|
||||
padding: 6,
|
||||
transition: transitions.create(['border-color', 'box-shadow']),
|
||||
border: `1px solid ${palette.divider}`,
|
||||
borderRadius: shape.borderRadius / 2,
|
||||
fontSize: 14,
|
||||
'&:focus': {
|
||||
boxShadow: `0px 0px 0px 3px ${palette.secondary[palette.mode]}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
const Label = memo(({ label, selected, unknown, ...props }) => {
|
||||
const [addLabel, { isLoading }] = useAddLabelMutation()
|
||||
|
||||
const StyledAutocompletePopper = styled('div')(({ theme }) => ({
|
||||
[`& .${autocompleteClasses.paper}`]: {
|
||||
boxShadow: 'none',
|
||||
margin: 0,
|
||||
color: 'inherit',
|
||||
fontSize: 13,
|
||||
},
|
||||
[`& .${autocompleteClasses.listbox}`]: {
|
||||
padding: 0,
|
||||
[`& .${autocompleteClasses.option}`]: {
|
||||
minHeight: 'auto',
|
||||
alignItems: 'flex-start',
|
||||
padding: 8,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
'&[aria-selected="true"]': {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
[`&.${autocompleteClasses.focused}, &.${autocompleteClasses.focused}[aria-selected="true"]`]:
|
||||
{
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
},
|
||||
},
|
||||
[`&.${autocompleteClasses.popperDisablePortal}`]: {
|
||||
position: 'relative',
|
||||
},
|
||||
}))
|
||||
/**
|
||||
* Adds the label to user labels.
|
||||
*
|
||||
* @param {MouseEvent<HTMLLIElement, MouseEvent>} evt - The click event
|
||||
*/
|
||||
const handleLockLabel = async (evt) => {
|
||||
evt.stopPropagation()
|
||||
await addLabel({ newLabel: label }).unwrap()
|
||||
}
|
||||
|
||||
const PopperComponent = ({ disablePortal, anchorEl, open, ...other }) => (
|
||||
<StyledAutocompletePopper {...other} />
|
||||
)
|
||||
return (
|
||||
<Box component="li" gap="0.5em" {...props}>
|
||||
<CheckIcon
|
||||
style={{
|
||||
minWidth: 'fit-content',
|
||||
visibility: selected ? 'visible' : 'hidden',
|
||||
}}
|
||||
/>
|
||||
<StatusCircle color={getColorFromString(label)} size={18} />
|
||||
<Typography noWrap variant="body2" sx={{ flexGrow: 1 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<SubmitButton
|
||||
onClick={unknown ? handleLockLabel : undefined}
|
||||
isSubmitting={isLoading}
|
||||
title={Tr(T.SavesInTheUserTemplate)}
|
||||
icon={<LockIcon />}
|
||||
sx={{ p: 0, visibility: unknown ? 'visible' : 'hidden' }}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
PopperComponent.propTypes = {
|
||||
anchorEl: PropTypes.any,
|
||||
disablePortal: PropTypes.bool,
|
||||
open: PropTypes.bool,
|
||||
Label.propTypes = {
|
||||
label: PropTypes.any,
|
||||
selected: PropTypes.bool,
|
||||
unknown: PropTypes.bool,
|
||||
}
|
||||
|
||||
Label.displayName = 'Label'
|
||||
|
||||
/**
|
||||
* AutoComplete to filter rows by label.
|
||||
*
|
||||
* @param {object} props - Component props
|
||||
* @param {string[]} props.currentValue - The current value of the filter
|
||||
* @param {function(SyntheticEvent, AutocompleteChangeReason, AutocompleteChangeDetails)} props.handleChange - Handle change event
|
||||
* @param {function(SyntheticEvent, AutocompleteCloseReason)} props.handleClose - Handle close event
|
||||
* @param {function(any)} props.handleChange - Handle change event
|
||||
* @param {string[]} props.pendingValue - The current value of the filter
|
||||
* @param {function()} props.handleClose - Handle close event
|
||||
* @param {string[]} props.labels - The list of labels to filter
|
||||
* @param {string[]} props.filters - The current filters
|
||||
* @param {string[]} props.unknownLabels - The list of labels not in the user labels
|
||||
* @returns {ReactElement} Filter component
|
||||
*/
|
||||
const FilterByLabel = ({
|
||||
currentValue = [],
|
||||
filters = [],
|
||||
labels = [],
|
||||
unknownLabels = [],
|
||||
pendingValue = [],
|
||||
handleChange,
|
||||
handleClose,
|
||||
}) => (
|
||||
<Autocomplete
|
||||
open
|
||||
multiple
|
||||
onClose={handleClose}
|
||||
value={currentValue}
|
||||
onChange={handleChange}
|
||||
value={pendingValue}
|
||||
onClose={(event, reason) => {
|
||||
reason === 'escape' && handleClose()
|
||||
}}
|
||||
onChange={(event, newValue, reason) => {
|
||||
if (
|
||||
event.type === 'keydown' &&
|
||||
event.key === 'Backspace' &&
|
||||
reason === 'removeOption'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
handleChange(newValue)
|
||||
}}
|
||||
disableCloseOnSelect
|
||||
PopperComponent={PopperComponent}
|
||||
renderTags={() => null}
|
||||
noOptionsText={<Translate word={T.NoLabels} />}
|
||||
renderOption={(props, option, { selected }) => (
|
||||
<Box component="li" gap="0.5em" {...props}>
|
||||
<CheckIcon style={{ visibility: selected ? 'visible' : 'hidden' }} />
|
||||
<StatusCircle color={getColorFromString(option)} size={18} />
|
||||
<Typography noWrap variant="body2" sx={{ flexGrow: 1 }}>
|
||||
{option}
|
||||
</Typography>
|
||||
<CancelIcon style={{ visibility: selected ? 'visible' : 'hidden' }} />
|
||||
</Box>
|
||||
<Label
|
||||
{...props}
|
||||
key={option}
|
||||
label={option}
|
||||
selected={selected}
|
||||
unknown={unknownLabels.includes(option)}
|
||||
/>
|
||||
)}
|
||||
isOptionEqualToValue={(option, value) =>
|
||||
Array.isArray(value) ? value.includes(option) : value === option
|
||||
}
|
||||
options={[...labels].sort((a, b) => {
|
||||
// Display the selected labels first.
|
||||
let ai = filters.indexOf(a)
|
||||
ai = ai === -1 ? filters.length + labels.indexOf(a) : ai
|
||||
let bi = filters.indexOf(b)
|
||||
bi = bi === -1 ? filters.length + labels.indexOf(b) : bi
|
||||
|
||||
return ai - bi
|
||||
})}
|
||||
options={labels}
|
||||
renderInput={(params) => (
|
||||
<StyledInput
|
||||
ref={params.InputProps.ref}
|
||||
inputProps={params.inputProps}
|
||||
autoFocus
|
||||
placeholder={Tr(T.FilterLabels)}
|
||||
placeholder={Tr(T.Search)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
FilterByLabel.propTypes = {
|
||||
currentValue: PropTypes.array,
|
||||
filters: PropTypes.array,
|
||||
labels: PropTypes.array,
|
||||
unknownLabels: PropTypes.array,
|
||||
pendingValue: PropTypes.array,
|
||||
handleChange: PropTypes.func,
|
||||
handleClose: PropTypes.func,
|
||||
}
|
||||
|
@ -13,107 +13,191 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useState, memo, useMemo } from 'react'
|
||||
import { ReactElement, useState, useMemo, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import SettingsIcon from 'iconoir-react/dist/Settings'
|
||||
import SettingsIcon from 'iconoir-react/dist/LabelOutline'
|
||||
import { Stack } from '@mui/material'
|
||||
import { UseFiltersInstanceProps } from 'react-table'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
|
||||
import Allocator from 'client/components/Tables/Enhanced/Utils/GlobalLabel/Allocator'
|
||||
import FilterByLabel from 'client/components/Tables/Enhanced/Utils/GlobalLabel/Filter'
|
||||
import HeaderPopover from 'client/components/Header/Popover'
|
||||
import { areStringEqual, jsonToXml } from 'client/models/Helper'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
export const LABEL_COLUMN_ID = 'label'
|
||||
|
||||
const getLabels = (rows) =>
|
||||
rows
|
||||
?.map((row) => row.values[LABEL_COLUMN_ID]?.split(','))
|
||||
const toUpperCase = (label) => label?.trim()?.toUpperCase()
|
||||
|
||||
const getLabelFromRows = (rows, flatting = true) => {
|
||||
const labels = rows
|
||||
?.map((row) => row.values[LABEL_COLUMN_ID]?.split(',') ?? [])
|
||||
.filter(Boolean)
|
||||
.flat()
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
|
||||
return flatting
|
||||
? labels.flat().map(toUpperCase)
|
||||
: labels.map((label) => [label].flat().filter(Boolean).map(toUpperCase))
|
||||
}
|
||||
|
||||
const sortByFilteredFirst = (labels, filters) =>
|
||||
labels.sort((a, b) => {
|
||||
let ai = filters.indexOf(a)
|
||||
ai = ai === -1 ? filters.length + labels.indexOf(a) : ai
|
||||
let bi = filters.indexOf(b)
|
||||
bi = bi === -1 ? filters.length + labels.indexOf(b) : bi
|
||||
|
||||
return ai - bi
|
||||
})
|
||||
|
||||
/**
|
||||
* Button to filter rows by label or assign labels to selected rows.
|
||||
*
|
||||
* @param {UseFiltersInstanceProps} props - Component props
|
||||
* @param {object} props.selectedRows - Selected rows
|
||||
* @param {Function} props.useUpdateMutation - Callback to update row labels
|
||||
* @returns {ReactElement} Button component
|
||||
*/
|
||||
const GlobalLabel = memo(
|
||||
(tableProps) => {
|
||||
const [pendingValue, setPendingValue] = useState([])
|
||||
const GlobalLabel = ({
|
||||
selectedRows = [],
|
||||
useUpdateMutation,
|
||||
...tableProps
|
||||
}) => {
|
||||
const { setFilter, page, state } = tableProps
|
||||
const [update, { isLoading } = {}] = useUpdateMutation?.() || []
|
||||
|
||||
/** @type {UseFiltersInstanceProps} */
|
||||
const { setFilter, preFilteredRows, state } = tableProps
|
||||
const [pendingValue, setPendingValue] = useState(() => [])
|
||||
const { labels: userLabels } = useAuth()
|
||||
|
||||
const labels = useMemo(
|
||||
() => [...new Set(getLabels(preFilteredRows))],
|
||||
[preFilteredRows]
|
||||
const enableEditLabel = useMemo(
|
||||
() => useUpdateMutation && selectedRows?.length > 0,
|
||||
[useUpdateMutation, selectedRows?.length]
|
||||
)
|
||||
|
||||
const unknownPageLabels = useMemo(
|
||||
() =>
|
||||
getLabelFromRows(page)
|
||||
.filter((label) => !userLabels.includes(label))
|
||||
.sort(areStringEqual),
|
||||
[page]
|
||||
)
|
||||
|
||||
const currentLabelFilters = useMemo(
|
||||
() =>
|
||||
state.filters
|
||||
.filter(({ id }) => id === LABEL_COLUMN_ID)
|
||||
.map(({ value }) => value)
|
||||
.flat(),
|
||||
[state.filters]
|
||||
)
|
||||
|
||||
const allFilterLabels = useMemo(() => {
|
||||
const all = [...userLabels, ...unknownPageLabels, ...currentLabelFilters]
|
||||
const unique = [...new Set(all)]
|
||||
|
||||
return sortByFilteredFirst(unique, currentLabelFilters)
|
||||
}, [userLabels, unknownPageLabels, currentLabelFilters])
|
||||
|
||||
const allocatorProps = useMemo(() => {
|
||||
if (!enableEditLabel) return {}
|
||||
|
||||
const selectedLabels = getLabelFromRows(selectedRows, false)
|
||||
const labels = sortByFilteredFirst(
|
||||
[...allFilterLabels],
|
||||
selectedLabels.flat()
|
||||
)
|
||||
|
||||
const filters = useMemo(
|
||||
() =>
|
||||
state.filters
|
||||
.filter(({ id }) => id === LABEL_COLUMN_ID)
|
||||
.map(({ value }) => value),
|
||||
[state.filters]
|
||||
return { selectedLabels, labels }
|
||||
}, [enableEditLabel, allFilterLabels, selectedRows])
|
||||
|
||||
/**
|
||||
* Handle event when user clicks on the label filter button
|
||||
*/
|
||||
const handleOpenPopover = useCallback(() => {
|
||||
if (!enableEditLabel) return setPendingValue(currentLabelFilters)
|
||||
|
||||
const { labels, selectedLabels } = allocatorProps
|
||||
const labelsInEveryRows = labels.filter((l) =>
|
||||
selectedLabels.every((selected) => selected.includes(l))
|
||||
)
|
||||
|
||||
if (labels.length === 0) {
|
||||
return null
|
||||
}
|
||||
// [labelsToAdd, labelsToRemove]
|
||||
setPendingValue([labelsInEveryRows, []])
|
||||
}, [enableEditLabel, currentLabelFilters, selectedRows])
|
||||
|
||||
return (
|
||||
<Stack direction="row" gap="0.5em" flexWrap="wrap">
|
||||
<HeaderPopover
|
||||
id="filter-by-label"
|
||||
icon={<SettingsIcon />}
|
||||
headerTitle={<Translate word={T.FilterByLabel} />}
|
||||
buttonLabel={<Translate word={T.Label} />}
|
||||
buttonProps={{
|
||||
'data-cy': 'filter-by-label',
|
||||
disableElevation: true,
|
||||
variant: filters?.length > 0 ? 'contained' : 'outlined',
|
||||
color: 'secondary',
|
||||
disabled: preFilteredRows?.length === 0,
|
||||
onClick: () => setPendingValue(filters),
|
||||
}}
|
||||
popperProps={{ placement: 'bottom-end' }}
|
||||
onClickAway={() => setFilter(LABEL_COLUMN_ID, pendingValue)}
|
||||
>
|
||||
{({ handleClose }) => (
|
||||
<FilterByLabel
|
||||
currentValue={pendingValue}
|
||||
labels={labels}
|
||||
filters={filters}
|
||||
handleChange={(event, newValue, reason) => {
|
||||
if (
|
||||
event.type === 'keydown' &&
|
||||
event.key === 'Backspace' &&
|
||||
reason === 'removeOption'
|
||||
) {
|
||||
return
|
||||
}
|
||||
/**
|
||||
* Handle event when user clicks outside of the popover
|
||||
*/
|
||||
const handleClickAwayPopover = useCallback(async () => {
|
||||
if (!enableEditLabel) return setFilter(LABEL_COLUMN_ID, pendingValue)
|
||||
|
||||
setPendingValue(newValue)
|
||||
}}
|
||||
handleClose={(event, reason) => {
|
||||
reason === 'escape' && handleClose()
|
||||
}}
|
||||
const [labelsToAdd, labelsToRemove] = pendingValue
|
||||
|
||||
await Promise.all(
|
||||
selectedRows.map(({ original: { ID, USER_TEMPLATE, TEMPLATE } }) => {
|
||||
const template = USER_TEMPLATE ?? TEMPLATE
|
||||
const currentLabels = template?.LABELS?.split(',') ?? []
|
||||
const newLabels = currentLabels
|
||||
.map((l) => l?.trim()?.toUpperCase())
|
||||
.filter((l) => labelsToRemove.indexOf(l) === -1)
|
||||
.concat(labelsToAdd)
|
||||
|
||||
const uniqueLabels = [...new Set(newLabels)].join(',')
|
||||
const templateXml = jsonToXml({ ...template, LABELS: uniqueLabels })
|
||||
|
||||
return update({ id: ID, template: templateXml, replace: 0 })
|
||||
})
|
||||
)
|
||||
}, [enableEditLabel, selectedRows, pendingValue, update])
|
||||
|
||||
return (
|
||||
<Stack direction="row" gap="0.5em" flexWrap="wrap">
|
||||
<HeaderPopover
|
||||
id="filter-by-label"
|
||||
icon={<SettingsIcon />}
|
||||
headerTitle={
|
||||
<Translate word={enableEditLabel ? T.ApplyLabels : T.FilterByLabel} />
|
||||
}
|
||||
buttonLabel={<Translate word={T.Label} />}
|
||||
buttonProps={{
|
||||
'data-cy': 'filter-by-label',
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
disabled: isLoading,
|
||||
onClick: handleOpenPopover,
|
||||
}}
|
||||
popperProps={{ placement: 'bottom-end' }}
|
||||
onClickAway={handleClickAwayPopover}
|
||||
>
|
||||
{({ handleClose }) =>
|
||||
enableEditLabel ? (
|
||||
<Allocator
|
||||
{...allocatorProps}
|
||||
pendingValue={pendingValue}
|
||||
handleChange={setPendingValue}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
</HeaderPopover>
|
||||
</Stack>
|
||||
)
|
||||
},
|
||||
(next, prev) =>
|
||||
next.preFilteredRows === prev.preFilteredRows &&
|
||||
next.state.filters === prev.state.filters
|
||||
)
|
||||
) : (
|
||||
<FilterByLabel
|
||||
labels={allFilterLabels}
|
||||
unknownLabels={unknownPageLabels}
|
||||
pendingValue={pendingValue}
|
||||
handleChange={setPendingValue}
|
||||
handleClose={handleClose}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</HeaderPopover>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
GlobalLabel.propTypes = {
|
||||
preFilteredRows: PropTypes.array,
|
||||
state: PropTypes.object,
|
||||
selectedRows: PropTypes.array,
|
||||
useUpdateMutation: PropTypes.func,
|
||||
}
|
||||
|
||||
GlobalLabel.displayName = 'GlobalLabel'
|
||||
|
@ -0,0 +1,85 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
styled,
|
||||
InputBase,
|
||||
PopperProps,
|
||||
autocompleteClasses,
|
||||
} from '@mui/material'
|
||||
|
||||
export const StyledInput = styled(InputBase)(
|
||||
({ theme: { shape, palette, transitions } }) => ({
|
||||
padding: 10,
|
||||
width: '100%',
|
||||
'& input': {
|
||||
padding: 6,
|
||||
transition: transitions.create(['border-color', 'box-shadow']),
|
||||
border: `1px solid ${palette.divider}`,
|
||||
borderRadius: shape.borderRadius / 2,
|
||||
fontSize: 14,
|
||||
'&:focus': {
|
||||
boxShadow: `0px 0px 0px 3px ${palette.secondary[palette.mode]}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
export const StyledAutocompletePopper = styled('div')(({ theme }) => ({
|
||||
[`& .${autocompleteClasses.paper}`]: {
|
||||
boxShadow: 'none',
|
||||
margin: 0,
|
||||
color: 'inherit',
|
||||
fontSize: 13,
|
||||
},
|
||||
[`& .${autocompleteClasses.listbox}`]: {
|
||||
padding: 0,
|
||||
[`& .${autocompleteClasses.option}`]: {
|
||||
minHeight: 'auto',
|
||||
alignItems: 'flex-start',
|
||||
padding: 8,
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
'&[aria-selected="true"]': {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
[`&.${autocompleteClasses.focused}, &.${autocompleteClasses.focused}[aria-selected="true"]`]:
|
||||
{
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
},
|
||||
},
|
||||
[`&.${autocompleteClasses.popperDisablePortal}`]: {
|
||||
position: 'relative',
|
||||
},
|
||||
}))
|
||||
|
||||
/**
|
||||
* @param {PopperProps} props - The props for the Popper component
|
||||
* @returns {ReactElement} Popper
|
||||
*/
|
||||
export const PopperComponent = ({
|
||||
disablePortal,
|
||||
anchorEl,
|
||||
open,
|
||||
...other
|
||||
}) => <StyledAutocompletePopper {...other} />
|
||||
|
||||
PopperComponent.propTypes = {
|
||||
anchorEl: PropTypes.any,
|
||||
disablePortal: PropTypes.bool,
|
||||
open: PropTypes.bool,
|
||||
}
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { JSXElementConstructor, useState, useCallback } from 'react'
|
||||
import { ReactElement, useState, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import clsx from 'clsx'
|
||||
@ -68,7 +68,7 @@ const useStyles = makeStyles(({ spacing, palette, shape, breakpoints }) => ({
|
||||
* @param {string} [props.className] - Class name for the container
|
||||
* @param {object} props.searchProps - Props for search input
|
||||
* @param {UseGlobalFiltersInstanceProps} props.useTableProps - Table props
|
||||
* @returns {JSXElementConstructor} Component JSX
|
||||
* @returns {ReactElement} Component JSX
|
||||
*/
|
||||
const GlobalSearch = ({ className, useTableProps, searchProps }) => {
|
||||
const classes = useStyles()
|
||||
|
@ -18,7 +18,8 @@ import { useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { InfoEmpty } from 'iconoir-react'
|
||||
import InfoEmpty from 'iconoir-react/dist/InfoEmpty'
|
||||
import RemoveIcon from 'iconoir-react/dist/RemoveSquare'
|
||||
import { Box } from '@mui/material'
|
||||
import {
|
||||
useGlobalFilter,
|
||||
@ -57,6 +58,7 @@ const EnhancedTable = ({
|
||||
initialState,
|
||||
refetch,
|
||||
isLoading,
|
||||
useUpdateMutation,
|
||||
displaySelectedRows,
|
||||
disableRowSelect,
|
||||
disableGlobalLabel,
|
||||
@ -109,6 +111,7 @@ const EnhancedTable = ({
|
||||
autoResetSelectedRows: false,
|
||||
autoResetSortBy: false,
|
||||
autoResetPage: false,
|
||||
autoResetGlobalFilter: false,
|
||||
// -------------------------------------
|
||||
initialState: { pageSize, ...initialState },
|
||||
},
|
||||
@ -123,12 +126,15 @@ const EnhancedTable = ({
|
||||
getTableProps,
|
||||
prepareRow,
|
||||
toggleAllRowsSelected,
|
||||
preFilteredRows,
|
||||
preGlobalFilteredRowsById,
|
||||
rows,
|
||||
page,
|
||||
gotoPage,
|
||||
pageCount,
|
||||
setFilter,
|
||||
setAllFilters,
|
||||
setSortBy,
|
||||
setGlobalFilter,
|
||||
state,
|
||||
} = useTableProps
|
||||
|
||||
@ -143,12 +149,18 @@ const EnhancedTable = ({
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
|
||||
useMountedLayoutEffect(() => {
|
||||
const selectedRows = preFilteredRows
|
||||
.filter((row) => !!state.selectedRowIds[row.id])
|
||||
.map((row) => ({ ...row, gotoPage: () => gotoRowPage(row) }))
|
||||
const selectedRows = useMemo(() => {
|
||||
const selectedIds = Object.keys(state.selectedRowIds ?? {})
|
||||
|
||||
onSelectedRowsChange?.(selectedRows)
|
||||
return selectedIds
|
||||
.map((id) => preGlobalFilteredRowsById[id])
|
||||
.filter(Boolean)
|
||||
}, [state.selectedRowIds])
|
||||
|
||||
useMountedLayoutEffect(() => {
|
||||
onSelectedRowsChange?.(
|
||||
selectedRows.map((row) => ({ ...row, gotoPage: () => gotoRowPage(row) }))
|
||||
)
|
||||
}, [state.selectedRowIds])
|
||||
|
||||
const handleChangePage = (newPage) => {
|
||||
@ -160,6 +172,18 @@ const EnhancedTable = ({
|
||||
newPage > state.pageIndex && !canNextPage && fetchMore?.()
|
||||
}
|
||||
|
||||
const handleResetFilters = () => {
|
||||
setGlobalFilter()
|
||||
setAllFilters([])
|
||||
setSortBy([])
|
||||
}
|
||||
|
||||
const cannotFilterByLabel = useMemo(
|
||||
() =>
|
||||
disableGlobalLabel || !columns.some((col) => col.id === LABEL_COLUMN_ID),
|
||||
[disableGlobalLabel]
|
||||
)
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...getTableProps()}
|
||||
@ -196,7 +220,13 @@ const EnhancedTable = ({
|
||||
|
||||
{/* FILTERS */}
|
||||
<div className={styles.filters}>
|
||||
{!disableGlobalLabel && <GlobalLabel {...useTableProps} />}
|
||||
{!cannotFilterByLabel && (
|
||||
<GlobalLabel
|
||||
{...useTableProps}
|
||||
selectedRows={selectedRows}
|
||||
useUpdateMutation={useUpdateMutation}
|
||||
/>
|
||||
)}
|
||||
<GlobalFilter {...useTableProps} />
|
||||
{!disableGlobalSort && <GlobalSort {...useTableProps} />}
|
||||
</div>
|
||||
@ -212,6 +242,20 @@ const EnhancedTable = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* RESET FILTERS */}
|
||||
<Box
|
||||
visibility={
|
||||
state.filters?.length > 0 || state.sortBy?.length > 0
|
||||
? 'visible'
|
||||
: 'hidden'
|
||||
}
|
||||
>
|
||||
<span className={styles.resetFilters} onClick={handleResetFilters}>
|
||||
<RemoveIcon />
|
||||
<Translate word={T.ResetFilters} />
|
||||
</span>
|
||||
</Box>
|
||||
|
||||
<div className={clsx(styles.body, classes.body)}>
|
||||
{/* NO DATA MESSAGE */}
|
||||
{!isLoading &&
|
||||
@ -245,15 +289,18 @@ const EnhancedTable = ({
|
||||
original={original}
|
||||
value={values}
|
||||
className={isSelected ? 'selected' : ''}
|
||||
onClickLabel={(label) => {
|
||||
const currentFilter =
|
||||
state.filters
|
||||
?.filter(({ id }) => id === LABEL_COLUMN_ID)
|
||||
?.map(({ value }) => value) || []
|
||||
{...(!cannotFilterByLabel && {
|
||||
onClickLabel: (label) => {
|
||||
const currentFilter =
|
||||
state.filters
|
||||
?.filter(({ id }) => id === LABEL_COLUMN_ID)
|
||||
?.map(({ value }) => value)
|
||||
?.flat() || []
|
||||
|
||||
const nextFilter = [...new Set([...currentFilter, label])]
|
||||
setFilter(LABEL_COLUMN_ID, nextFilter)
|
||||
}}
|
||||
const nextFilter = [...new Set([...currentFilter, label])]
|
||||
setFilter(LABEL_COLUMN_ID, nextFilter)
|
||||
},
|
||||
})}
|
||||
onClick={() => {
|
||||
typeof onRowClick === 'function' && onRowClick(original)
|
||||
|
||||
@ -295,6 +342,7 @@ EnhancedTable.propTypes = {
|
||||
disableGlobalSort: PropTypes.bool,
|
||||
disableRowSelect: PropTypes.bool,
|
||||
displaySelectedRows: PropTypes.bool,
|
||||
useUpdateMutation: PropTypes.func,
|
||||
onSelectedRowsChange: PropTypes.func,
|
||||
onRowClick: PropTypes.func,
|
||||
pageSize: PropTypes.number,
|
||||
|
@ -24,7 +24,7 @@ export default makeStyles(({ palette, typography, breakpoints }) => ({
|
||||
},
|
||||
toolbar: {
|
||||
...typography.body1,
|
||||
marginBottom: 16,
|
||||
marginBottom: '1em',
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto auto',
|
||||
gridTemplateAreas: `
|
||||
@ -55,6 +55,16 @@ export default makeStyles(({ palette, typography, breakpoints }) => ({
|
||||
justifySelf: 'end',
|
||||
gap: '1em',
|
||||
},
|
||||
resetFilters: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '1em',
|
||||
'&:hover': {
|
||||
color: palette.secondary.dark,
|
||||
},
|
||||
},
|
||||
body: {
|
||||
overflow: 'auto',
|
||||
display: 'grid',
|
||||
|
@ -15,7 +15,6 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
// INFORMATION
|
||||
export const REFRESH = 'refresh'
|
||||
export const RENAME = 'rename'
|
||||
|
||||
// ATTRIBUTES
|
||||
|
@ -15,7 +15,6 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
// eslint-disable-next-line prettier/prettier, no-unused-vars
|
||||
import { VmQuota, NetworkQuota, DatastoreQuota, ImageQuota } from 'client/constants/quota'
|
||||
import * as ACTIONS from 'client/constants/actions'
|
||||
|
||||
/**
|
||||
* @typedef Group
|
||||
@ -37,7 +36,6 @@ import * as ACTIONS from 'client/constants/actions'
|
||||
*/
|
||||
|
||||
export const GROUP_ACTIONS = {
|
||||
REFRESH: ACTIONS.REFRESH,
|
||||
CREATE_DIALOG: 'create_dialog',
|
||||
UPDATE_DIALOG: 'update_dialog',
|
||||
QUOTAS_DIALOG: 'quotas_dialog',
|
||||
|
@ -174,7 +174,6 @@ export const HOST_STATES = [
|
||||
|
||||
/** @enum {string} Host actions */
|
||||
export const HOST_ACTIONS = {
|
||||
REFRESH: ACTIONS.REFRESH,
|
||||
CREATE_DIALOG: 'create_dialog',
|
||||
RENAME: ACTIONS.RENAME,
|
||||
CHANGE_CLUSTER: 'change_cluster',
|
||||
|
@ -59,6 +59,7 @@ export const SCHEMES = Setting.SCHEMES
|
||||
export const DEFAULT_SCHEME = Setting.SCHEMES.SYSTEM
|
||||
|
||||
export const CURRENCY = SERVER_CONFIG?.currency ?? 'EUR'
|
||||
export const LOCALE = SERVER_CONFIG?.lang?.replace('_', '-') ?? undefined
|
||||
export const DEFAULT_LANGUAGE = SERVER_CONFIG?.default_lang ?? 'en'
|
||||
export const LANGUAGES_URL = `${STATIC_FILES_URL}/languages`
|
||||
export const LANGUAGES = SERVER_CONFIG.langs ?? {
|
||||
|
@ -49,9 +49,17 @@ import * as ACTIONS from 'client/constants/actions'
|
||||
|
||||
/** @enum {string} Marketplace App actions */
|
||||
export const MARKETPLACE_APP_ACTIONS = {
|
||||
REFRESH: ACTIONS.REFRESH,
|
||||
CREATE_DIALOG: 'create_dialog',
|
||||
RENAME: ACTIONS.RENAME,
|
||||
EXPORT: 'export',
|
||||
DOWNLOAD: 'download',
|
||||
ENABLE: 'enable',
|
||||
DISABLE: 'disable',
|
||||
DELETE: 'delete',
|
||||
EDIT_LABELS: 'edit_labels',
|
||||
|
||||
// INFORMATION
|
||||
RENAME: ACTIONS.RENAME,
|
||||
CHANGE_MODE: ACTIONS.CHANGE_MODE,
|
||||
CHANGE_OWNER: ACTIONS.CHANGE_OWNER,
|
||||
CHANGE_GROUP: ACTIONS.CHANGE_GROUP,
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ module.exports = {
|
||||
FilterBy: 'Filter by',
|
||||
FilterLabels: 'Filter labels',
|
||||
FilterByLabel: 'Filter by label',
|
||||
ApplyLabels: 'Apply labels',
|
||||
Label: 'Label',
|
||||
NoLabels: 'NoLabels',
|
||||
All: 'All',
|
||||
@ -33,6 +34,7 @@ module.exports = {
|
||||
NumberOfResourcesSelected: 'All %s resources are selected',
|
||||
SelectAllResources: 'Select all %s resources',
|
||||
ClearSelection: 'Clear selection',
|
||||
ResetFilters: 'Clear current search query, filters, and sorts',
|
||||
|
||||
/* actions */
|
||||
Accept: 'Accept',
|
||||
@ -279,6 +281,7 @@ module.exports = {
|
||||
CleanupConcept: 'Delete all vms and images first, then delete the resources',
|
||||
Force: 'Force',
|
||||
ForceConcept: 'Force configure to execute',
|
||||
ConfigureProvision: 'Configure provision %s',
|
||||
|
||||
/* sections */
|
||||
Dashboard: 'Dashboard',
|
||||
@ -297,6 +300,11 @@ module.exports = {
|
||||
AddUserSshPrivateKey: 'Add user SSH private key',
|
||||
SshPassphraseKey: 'SSH private key passphrase',
|
||||
AddUserSshPassphraseKey: 'Add user SSH private key passphrase',
|
||||
Labels: 'Labels',
|
||||
NewLabelOrSearch: 'New label or search',
|
||||
LabelAlreadyExists: 'Label already exists',
|
||||
PressToCreateLabel: 'Press enter to create a new label',
|
||||
SavesInTheUserTemplate: "Saved in the User's template",
|
||||
|
||||
/* sections - system */
|
||||
User: 'User',
|
||||
|
@ -50,7 +50,6 @@ import * as ACTIONS from 'client/constants/actions'
|
||||
*/
|
||||
|
||||
export const USER_ACTIONS = {
|
||||
REFRESH: ACTIONS.REFRESH,
|
||||
CREATE_DIALOG: 'create_dialog',
|
||||
QUOTAS_DIALOG: 'quotas_dialog',
|
||||
GROUPS_DIALOG: 'groups_dialog',
|
||||
|
@ -713,7 +713,6 @@ export const VM_LCM_STATES = [
|
||||
|
||||
/** @enum {string} Virtual machine actions */
|
||||
export const VM_ACTIONS = {
|
||||
REFRESH: ACTIONS.REFRESH,
|
||||
CREATE_DIALOG: 'create_dialog',
|
||||
CREATE_APP_DIALOG: 'create_app_dialog',
|
||||
DEPLOY: 'deploy',
|
||||
|
@ -45,7 +45,6 @@ import { Permissions, LockInfo } from 'client/constants/common'
|
||||
*/
|
||||
|
||||
export const VM_TEMPLATE_ACTIONS = {
|
||||
REFRESH: ACTIONS.REFRESH,
|
||||
CREATE_DIALOG: 'create_dialog',
|
||||
IMPORT_DIALOG: 'import_dialog',
|
||||
UPDATE_DIALOG: 'update_dialog',
|
||||
|
@ -15,18 +15,24 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useState, memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { BookmarkEmpty } from 'iconoir-react'
|
||||
import { Typography, Box, Stack, Chip, IconButton } from '@mui/material'
|
||||
import GotoIcon from 'iconoir-react/dist/Pin'
|
||||
import RefreshDouble from 'iconoir-react/dist/RefreshDouble'
|
||||
import Cancel from 'iconoir-react/dist/Cancel'
|
||||
import { Typography, Box, Stack, Chip } from '@mui/material'
|
||||
import { Row } from 'react-table'
|
||||
|
||||
import hostApi from 'client/features/OneApi/host'
|
||||
import {
|
||||
useLazyGetHostQuery,
|
||||
useUpdateHostMutation,
|
||||
} from 'client/features/OneApi/host'
|
||||
import { HostsTable } from 'client/components/Tables'
|
||||
import HostTabs from 'client/components/Tabs/Host'
|
||||
import HostActions from 'client/components/Tables/Hosts/actions'
|
||||
import SplitPane from 'client/components/SplitPane'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
import { T, Host } from 'client/constants'
|
||||
|
||||
/**
|
||||
* Displays a list of Hosts with a split pane between the list and selected row(s).
|
||||
@ -47,6 +53,7 @@ function Hosts() {
|
||||
<HostsTable
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
globalActions={actions}
|
||||
useUpdateMutation={useUpdateHostMutation}
|
||||
/>
|
||||
|
||||
{hasSelectedRows && (
|
||||
@ -56,8 +63,9 @@ function Hosts() {
|
||||
<GroupedTags tags={selectedRows} />
|
||||
) : (
|
||||
<InfoTabs
|
||||
id={selectedRows[0]?.original?.ID}
|
||||
host={selectedRows[0]?.original}
|
||||
gotoPage={selectedRows[0]?.gotoPage}
|
||||
unselect={() => selectedRows[0]?.toggleRowSelected(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@ -71,26 +79,45 @@ function Hosts() {
|
||||
/**
|
||||
* Displays details of a Host.
|
||||
*
|
||||
* @param {string} id - Host id to display
|
||||
* @param {Host} host - Host to display
|
||||
* @param {Function} [gotoPage] - Function to navigate to a page of a Host
|
||||
* @param {Function} [unselect] - Function to unselect a Host
|
||||
* @returns {ReactElement} Host details
|
||||
*/
|
||||
const InfoTabs = memo(({ id, gotoPage }) => {
|
||||
const host = hostApi.endpoints.getHosts.useQueryState(undefined, {
|
||||
selectFromResult: ({ data = [] }) => data.find((item) => +item.ID === +id),
|
||||
})
|
||||
const InfoTabs = memo(({ host, gotoPage, unselect }) => {
|
||||
const [getVm, { data: lazyData, isFetching }] = useLazyGetHostQuery()
|
||||
const id = lazyData?.ID ?? host.ID
|
||||
const name = lazyData?.NAME ?? host.NAME
|
||||
|
||||
return (
|
||||
<Stack overflow="auto">
|
||||
<Stack direction="row" alignItems="center" gap={1} mb={1}>
|
||||
<Typography color="text.primary" noWrap>
|
||||
{`#${id} | ${host.NAME}`}
|
||||
</Typography>
|
||||
{gotoPage && (
|
||||
<IconButton title={Tr(T.LocateOnTable)} onClick={gotoPage}>
|
||||
<BookmarkEmpty />
|
||||
</IconButton>
|
||||
<SubmitButton
|
||||
data-cy="detail-refresh"
|
||||
icon={<RefreshDouble />}
|
||||
tooltip={Tr(T.Refresh)}
|
||||
isSubmitting={isFetching}
|
||||
onClick={() => getVm({ id })}
|
||||
/>
|
||||
{typeof gotoPage === 'function' && (
|
||||
<SubmitButton
|
||||
data-cy="locate-on-table"
|
||||
icon={<GotoIcon />}
|
||||
tooltip={Tr(T.LocateOnTable)}
|
||||
onClick={() => gotoPage()}
|
||||
/>
|
||||
)}
|
||||
{typeof unselect === 'function' && (
|
||||
<SubmitButton
|
||||
data-cy="unselect"
|
||||
icon={<Cancel />}
|
||||
tooltip={Tr(T.Close)}
|
||||
onClick={() => unselect()}
|
||||
/>
|
||||
)}
|
||||
<Typography color="text.primary" noWrap>
|
||||
{`#${id} | ${name}`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<HostTabs id={id} />
|
||||
</Stack>
|
||||
@ -98,8 +125,9 @@ const InfoTabs = memo(({ id, gotoPage }) => {
|
||||
})
|
||||
|
||||
InfoTabs.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
host: PropTypes.object,
|
||||
gotoPage: PropTypes.func,
|
||||
unselect: PropTypes.func,
|
||||
}
|
||||
|
||||
InfoTabs.displayName = 'InfoTabs'
|
||||
|
@ -195,7 +195,7 @@ const AppLinks = () => {
|
||||
return otherApps.map((app) => (
|
||||
<Link
|
||||
key={app}
|
||||
href={`${APP_URL}/${app}`}
|
||||
href={`${APP_URL}/${app}`.toLowerCase()}
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
padding={1}
|
||||
|
@ -15,18 +15,24 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useState, memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { BookmarkEmpty } from 'iconoir-react'
|
||||
import { Typography, Box, Stack, Chip, IconButton } from '@mui/material'
|
||||
import GotoIcon from 'iconoir-react/dist/Pin'
|
||||
import RefreshDouble from 'iconoir-react/dist/RefreshDouble'
|
||||
import Cancel from 'iconoir-react/dist/Cancel'
|
||||
import { Typography, Box, Stack, Chip } from '@mui/material'
|
||||
import { Row } from 'react-table'
|
||||
|
||||
import marketAppApi from 'client/features/OneApi/marketplaceApp'
|
||||
import {
|
||||
useUpdateAppMutation,
|
||||
useLazyGetMarketplaceAppQuery,
|
||||
} from 'client/features/OneApi/marketplaceApp'
|
||||
import { MarketplaceAppsTable } from 'client/components/Tables'
|
||||
import MarketplaceAppActions from 'client/components/Tables/MarketplaceApps/actions'
|
||||
import MarketplaceAppsTabs from 'client/components/Tabs/MarketplaceApp'
|
||||
import SplitPane from 'client/components/SplitPane'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
import { T, MarketplaceApp } from 'client/constants'
|
||||
|
||||
/**
|
||||
* Displays a list of Marketplace Apps with a split pane between the list and selected row(s).
|
||||
@ -47,6 +53,7 @@ function MarketplaceApps() {
|
||||
<MarketplaceAppsTable
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
globalActions={actions}
|
||||
useUpdateMutation={useUpdateAppMutation}
|
||||
/>
|
||||
|
||||
{hasSelectedRows && (
|
||||
@ -56,8 +63,9 @@ function MarketplaceApps() {
|
||||
<GroupedTags tags={selectedRows} />
|
||||
) : (
|
||||
<InfoTabs
|
||||
id={selectedRows[0]?.original?.ID}
|
||||
app={selectedRows[0]?.original}
|
||||
gotoPage={selectedRows[0]?.gotoPage}
|
||||
unselect={() => selectedRows[0]?.toggleRowSelected(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@ -71,30 +79,47 @@ function MarketplaceApps() {
|
||||
/**
|
||||
* Displays details of a Marketplace App.
|
||||
*
|
||||
* @param {string} id - Marketplace App id to display
|
||||
* @param {MarketplaceApp} app - Marketplace App to display
|
||||
* @param {Function} [gotoPage] - Function to navigate to a page of a Marketplace App
|
||||
* @param {Function} [unselect] - Function to unselect a Marketplace App
|
||||
* @returns {ReactElement} Marketplace App details
|
||||
*/
|
||||
const InfoTabs = memo(({ id, gotoPage }) => {
|
||||
const app = marketAppApi.endpoints.getMarketplaceApps.useQueryState(
|
||||
undefined,
|
||||
{
|
||||
selectFromResult: ({ data = [] }) =>
|
||||
data.find((item) => +item.ID === +id),
|
||||
}
|
||||
)
|
||||
const InfoTabs = memo(({ app, gotoPage, unselect }) => {
|
||||
const [getApp, queryState] = useLazyGetMarketplaceAppQuery()
|
||||
const { data: lazyData, isFetching } = queryState
|
||||
|
||||
const id = lazyData?.ID ?? app.ID
|
||||
const name = lazyData?.NAME ?? app.NAME
|
||||
|
||||
return (
|
||||
<Stack overflow="auto">
|
||||
<Stack direction="row" alignItems="center" gap={1} mb={1}>
|
||||
<Typography color="text.primary" noWrap>
|
||||
{`#${id} | ${app.NAME}`}
|
||||
</Typography>
|
||||
{gotoPage && (
|
||||
<IconButton title={Tr(T.LocateOnTable)} onClick={gotoPage}>
|
||||
<BookmarkEmpty />
|
||||
</IconButton>
|
||||
<SubmitButton
|
||||
data-cy="detail-refresh"
|
||||
icon={<RefreshDouble />}
|
||||
tooltip={Tr(T.Refresh)}
|
||||
isSubmitting={isFetching}
|
||||
onClick={() => getApp({ id })}
|
||||
/>
|
||||
{typeof gotoPage === 'function' && (
|
||||
<SubmitButton
|
||||
data-cy="locate-on-table"
|
||||
icon={<GotoIcon />}
|
||||
tooltip={Tr(T.LocateOnTable)}
|
||||
onClick={() => gotoPage()}
|
||||
/>
|
||||
)}
|
||||
{typeof unselect === 'function' && (
|
||||
<SubmitButton
|
||||
data-cy="unselect"
|
||||
icon={<Cancel />}
|
||||
tooltip={Tr(T.Close)}
|
||||
onClick={() => unselect()}
|
||||
/>
|
||||
)}
|
||||
<Typography color="text.primary" noWrap>
|
||||
{`#${id} | ${name}`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<MarketplaceAppsTabs id={id} />
|
||||
</Stack>
|
||||
@ -102,8 +127,9 @@ const InfoTabs = memo(({ id, gotoPage }) => {
|
||||
})
|
||||
|
||||
InfoTabs.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
app: PropTypes.object,
|
||||
gotoPage: PropTypes.func,
|
||||
unselect: PropTypes.func,
|
||||
}
|
||||
|
||||
InfoTabs.displayName = 'InfoTabs'
|
||||
|
@ -49,7 +49,10 @@ function ProviderCreateForm() {
|
||||
const { enqueueSuccess, enqueueError } = useGeneralApi()
|
||||
|
||||
const [createProvider] = useCreateProviderMutation()
|
||||
const [updateProvider] = useUpdateProviderMutation()
|
||||
const [
|
||||
updateProvider,
|
||||
{ isSuccess: successUpdate, originalArgs: { id: updatedProviderId } = {} },
|
||||
] = useUpdateProviderMutation()
|
||||
|
||||
const { data: providerConfig, error: errorConfig } =
|
||||
useGetProviderConfigQuery(undefined, { refetchOnMountOrArgChange: false })
|
||||
@ -71,7 +74,6 @@ function ProviderCreateForm() {
|
||||
|
||||
if (id !== undefined) {
|
||||
await updateProvider({ id, data: submitData })
|
||||
enqueueSuccess(`Provider updated - ID: ${id}`)
|
||||
} else {
|
||||
if (!isValidProviderTemplate(submitData, providerConfig)) {
|
||||
enqueueError(
|
||||
@ -81,7 +83,7 @@ function ProviderCreateForm() {
|
||||
}
|
||||
|
||||
const responseId = await createProvider({ data: submitData }).unwrap()
|
||||
enqueueSuccess(`Provider created - ID: ${responseId}`)
|
||||
responseId && enqueueSuccess(`Provider created - ID: ${responseId}`)
|
||||
}
|
||||
|
||||
history.push(PATH.PROVIDERS.LIST)
|
||||
@ -93,6 +95,11 @@ function ProviderCreateForm() {
|
||||
id && getConnection(id)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
successUpdate &&
|
||||
enqueueSuccess(`Provider updated - ID: ${updatedProviderId}`)
|
||||
}, [successUpdate])
|
||||
|
||||
if (errorConfig || errorConnection || errorProvider) {
|
||||
return <Redirect to={PATH.PROVIDERS.LIST} />
|
||||
}
|
||||
|
@ -49,8 +49,14 @@ function Providers() {
|
||||
|
||||
const { enqueueSuccess } = useGeneralApi()
|
||||
const { data: providerConfig } = useGetProviderConfigQuery()
|
||||
const [deleteProvider, { isLoading: isDeleting }] =
|
||||
useDeleteProviderMutation()
|
||||
const [
|
||||
deleteProvider,
|
||||
{
|
||||
isLoading: isDeleting,
|
||||
isSuccess: successDelete,
|
||||
originalArgs: { id: deletedProviderId } = {},
|
||||
},
|
||||
] = useDeleteProviderMutation()
|
||||
|
||||
const {
|
||||
refetch,
|
||||
@ -68,10 +74,14 @@ function Providers() {
|
||||
try {
|
||||
hide()
|
||||
await deleteProvider({ id })
|
||||
enqueueSuccess(`Provider deleted - ID: ${id}`)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
successDelete &&
|
||||
enqueueSuccess(`Provider deleted - ID: ${deletedProviderId}`)
|
||||
}, [successDelete])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListHeader
|
||||
@ -110,6 +120,7 @@ function Providers() {
|
||||
handleClick: () =>
|
||||
show({
|
||||
id: ID,
|
||||
delete: true,
|
||||
title: (
|
||||
<Translate
|
||||
word={T.DeleteSomething}
|
||||
@ -128,13 +139,21 @@ function Providers() {
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{display && dialogProps?.id && (
|
||||
<DialogProvider
|
||||
hide={hide}
|
||||
id={dialogProps.id}
|
||||
dialogProps={dialogProps}
|
||||
/>
|
||||
)}
|
||||
{display &&
|
||||
dialogProps?.id &&
|
||||
(!dialogProps?.delete ? (
|
||||
<DialogProvider
|
||||
hide={hide}
|
||||
id={dialogProps.id}
|
||||
dialogProps={dialogProps}
|
||||
/>
|
||||
) : (
|
||||
<DialogConfirmation handleCancel={hide} {...dialogProps}>
|
||||
<p>
|
||||
<Translate word={T.DoYouWantProceed} />
|
||||
</p>
|
||||
</DialogConfirmation>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo } from 'react'
|
||||
import { memo, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Trash as DeleteIcon } from 'iconoir-react'
|
||||
@ -33,8 +33,14 @@ const Datastores = memo(
|
||||
({ id }) => {
|
||||
const { enqueueSuccess } = useGeneralApi()
|
||||
|
||||
const [removeResource, { isLoading: loadingRemove }] =
|
||||
useRemoveResourceMutation()
|
||||
const [
|
||||
removeResource,
|
||||
{
|
||||
isLoading: loadingRemove,
|
||||
isSuccess: successRemove,
|
||||
originalArgs: { id: deletedDatastoreId } = {},
|
||||
},
|
||||
] = useRemoveResourceMutation()
|
||||
const { data } = useGetProvisionQuery(id)
|
||||
|
||||
const provisionDatastores =
|
||||
@ -42,6 +48,11 @@ const Datastores = memo(
|
||||
(datastore) => +datastore.id
|
||||
) ?? []
|
||||
|
||||
useEffect(() => {
|
||||
successRemove &&
|
||||
enqueueSuccess(`Datastore deleted - ID: ${deletedDatastoreId}`)
|
||||
}, [successRemove])
|
||||
|
||||
return (
|
||||
<DatastoresTable
|
||||
disableGlobalSort
|
||||
@ -76,7 +87,6 @@ const Datastores = memo(
|
||||
id: datastore.ID,
|
||||
resource: 'datastore',
|
||||
})
|
||||
enqueueSuccess(`Datastore deleted - ID: ${datastore.ID}`)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ import { T } from 'client/constants'
|
||||
|
||||
const Hosts = memo(({ id }) => {
|
||||
const [amount, setAmount] = useState(() => 1)
|
||||
const { enqueueSuccess, enqueueInfo } = useGeneralApi()
|
||||
const { enqueueInfo } = useGeneralApi()
|
||||
|
||||
const [addHost, { isLoading: loadingAddHost }] =
|
||||
useAddHostToProvisionMutation()
|
||||
@ -77,7 +77,7 @@ const Hosts = memo(({ id }) => {
|
||||
isSubmitting={loadingAddHost}
|
||||
onClick={async () => {
|
||||
addHost({ id, amount })
|
||||
enqueueSuccess(`Host added ${amount}x`)
|
||||
enqueueInfo(`Adding ${amount} Host${amount > 1 ? 's' : ''}`)
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
@ -125,7 +125,7 @@ const Hosts = memo(({ id }) => {
|
||||
id: host.ID,
|
||||
resource: 'host',
|
||||
})
|
||||
enqueueSuccess(`Host deleted - ID: ${host.ID}`)
|
||||
enqueueInfo(`Deleting Host - ID:${host.ID}`)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
@ -28,7 +28,7 @@ import {
|
||||
import { useSearch, useDialog } from 'client/hooks'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
import { DeleteForm } from 'client/components/Forms/Provision'
|
||||
import { DeleteForm, ConfigureForm } from 'client/components/Forms/Provision'
|
||||
import { ListHeader, ListCards } from 'client/components/List'
|
||||
import AlertError from 'client/components/Alerts/Error'
|
||||
import { ProvisionCard } from 'client/components/Cards'
|
||||
@ -104,16 +104,33 @@ function Provisions() {
|
||||
CardComponent={ProvisionCard}
|
||||
cardsProps={({ value: { ID, NAME } }) => ({
|
||||
handleClick: () => handleClickfn(ID, NAME),
|
||||
actions: [
|
||||
{
|
||||
handleClick: async () => {
|
||||
await configureProvision({ id: ID })
|
||||
enqueueInfo(`Configuring provision - ID: ${ID}`)
|
||||
},
|
||||
configureAction: {
|
||||
buttonProps: {
|
||||
'data-cy': 'provision-configure',
|
||||
icon: <EditIcon />,
|
||||
cy: 'provision-configure',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
dialogProps: {
|
||||
title: (
|
||||
<Translate
|
||||
word={T.ConfigureProvision}
|
||||
values={`#${ID} ${NAME}`}
|
||||
/>
|
||||
),
|
||||
},
|
||||
form: ConfigureForm,
|
||||
onSubmit: async (formData) => {
|
||||
try {
|
||||
await configureProvision({ id: ID, ...formData })
|
||||
enqueueInfo(`Configuring provision - ID: ${ID}`)
|
||||
} finally {
|
||||
hide()
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
deleteAction: {
|
||||
buttonProps: {
|
||||
'data-cy': 'provision-delete',
|
||||
|
@ -195,7 +195,10 @@ const Settings = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper variant="outlined" sx={{ py: '1.5em' }}>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{ overflow: 'auto', py: '1.5em', gridColumn: { md: 'span 2' } }}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<Stack gap="1em">
|
||||
{FIELDS.map((field) => (
|
||||
|
@ -79,7 +79,7 @@ const Settings = () => {
|
||||
component="form"
|
||||
onSubmit={handleSubmit(handleUpdateUser)}
|
||||
variant="outlined"
|
||||
sx={{ p: '1em', maxWidth: { sm: 'auto', md: 550 } }}
|
||||
sx={{ p: '1em' }}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema
|
||||
|
@ -0,0 +1,171 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useEffect, useCallback } from 'react'
|
||||
import TrashIcon from 'iconoir-react/dist/Trash'
|
||||
import { styled, Paper, Stack, Box, Typography, TextField } from '@mui/material'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import {
|
||||
useAddLabelMutation,
|
||||
useRemoveLabelMutation,
|
||||
} from 'client/features/OneApi/auth'
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { useSearch } from 'client/hooks'
|
||||
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
import { getColorFromString } from 'client/models/Helper'
|
||||
import { Translate, Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const NEW_LABEL_ID = 'new-label'
|
||||
|
||||
const LabelWrapper = styled(Box)(({ theme, ownerState }) => ({
|
||||
display: 'flex',
|
||||
direction: 'row',
|
||||
alignItems: 'center',
|
||||
paddingInline: '0.5rem',
|
||||
borderRadius: theme.shape.borderRadius * 2,
|
||||
animation: ownerState.highlight ? 'highlight 2s ease-in-out' : undefined,
|
||||
'@keyframes highlight': {
|
||||
from: { backgroundColor: 'yellow' },
|
||||
to: { backgroundColor: 'transparent' },
|
||||
},
|
||||
}))
|
||||
|
||||
/**
|
||||
* Section to change labels.
|
||||
*
|
||||
* @returns {ReactElement} Settings configuration UI
|
||||
*/
|
||||
const Settings = () => {
|
||||
const { labels } = useAuth()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
const [removeLabel, { isLoading: removeLoading }] = useRemoveLabelMutation()
|
||||
const [addLabel, { isLoading, data, isSuccess }] = useAddLabelMutation()
|
||||
|
||||
const { handleSubmit, register, reset, setFocus } = useForm({
|
||||
reValidateMode: 'onSubmit',
|
||||
})
|
||||
|
||||
const { result, handleChange } = useSearch({
|
||||
list: labels,
|
||||
listOptions: { threshold: 0.2 },
|
||||
wait: 400,
|
||||
condition: !isLoading,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSuccess) return
|
||||
|
||||
setTimeout(() => {
|
||||
// scroll to the new label (if it exists)
|
||||
document
|
||||
?.querySelector(`[data-cy='${data}']`)
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}, 450)
|
||||
}, [isSuccess])
|
||||
|
||||
const handleAddLabel = useCallback(
|
||||
async (formData) => {
|
||||
try {
|
||||
await addLabel({ newLabel: formData[NEW_LABEL_ID] }).unwrap()
|
||||
} catch (error) {
|
||||
enqueueError(error.message ?? T.SomethingWrong)
|
||||
} finally {
|
||||
// Reset the search after adding the label
|
||||
handleChange()
|
||||
reset({ [NEW_LABEL_ID]: '' })
|
||||
setFocus(NEW_LABEL_ID)
|
||||
}
|
||||
},
|
||||
[addLabel, handleChange, reset]
|
||||
)
|
||||
|
||||
const handleDeleteLabel = useCallback(
|
||||
async (label) => {
|
||||
try {
|
||||
await removeLabel({ label }).unwrap()
|
||||
} catch (error) {
|
||||
enqueueError(error.message ?? T.SomethingWrong)
|
||||
}
|
||||
},
|
||||
[removeLabel, handleChange]
|
||||
)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(evt) => evt.key === 'Enter' && handleSubmit(handleAddLabel)(evt),
|
||||
[handleAddLabel, handleSubmit]
|
||||
)
|
||||
|
||||
return (
|
||||
<Paper variant="outlined" sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Box mt="0.5rem" p="1rem">
|
||||
<Typography variant="underline">
|
||||
<Translate word={T.Labels} />
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack height={1} gap="0.5rem" p="0.5rem" overflow="auto">
|
||||
{labels.length === 0 && (
|
||||
<Typography variant="subtitle2">
|
||||
<Translate word={T.NoLabels} />
|
||||
</Typography>
|
||||
)}
|
||||
{result?.map((label) => (
|
||||
<LabelWrapper
|
||||
key={label}
|
||||
data-cy={`wrapper-${label}`}
|
||||
// highlight the label when it is added
|
||||
ownerState={{ highlight: data === label }}
|
||||
>
|
||||
<Box display="inline-flex" flexGrow={1} width="80%">
|
||||
<StatusChip
|
||||
noWrap
|
||||
dataCy={label}
|
||||
text={label}
|
||||
stateColor={getColorFromString(label)}
|
||||
/>
|
||||
</Box>
|
||||
<SubmitButton
|
||||
data-cy={`delete-label-${label}`}
|
||||
disabled={removeLoading}
|
||||
onClick={() => handleDeleteLabel(label)}
|
||||
icon={<TrashIcon />}
|
||||
/>
|
||||
</LabelWrapper>
|
||||
))}
|
||||
</Stack>
|
||||
<TextField
|
||||
sx={{ flexGrow: 1, p: '0.5rem 1rem' }}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isLoading}
|
||||
placeholder={Tr(T.NewLabelOrSearch)}
|
||||
inputProps={{ 'data-cy': NEW_LABEL_ID }}
|
||||
InputProps={{
|
||||
endAdornment: isLoading ? (
|
||||
<CircularProgress color="secondary" size={14} />
|
||||
) : undefined,
|
||||
}}
|
||||
{...register(NEW_LABEL_ID, { onChange: handleChange })}
|
||||
helperText={Tr(T.PressToCreateLabel)}
|
||||
/>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings
|
@ -14,13 +14,14 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement } from 'react'
|
||||
import { Typography, Divider, Stack } from '@mui/material'
|
||||
import { Typography, Divider, Box } from '@mui/material'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
import ConfigurationUISection from 'client/containers/Settings/ConfigurationUI'
|
||||
import AuthenticationSection from 'client/containers/Settings/Authentication'
|
||||
import LabelsSection from 'client/containers/Settings/LabelsSection'
|
||||
|
||||
/** @returns {ReactElement} Settings container */
|
||||
const Settings = () => (
|
||||
@ -31,10 +32,16 @@ const Settings = () => (
|
||||
|
||||
<Divider sx={{ my: '1em' }} />
|
||||
|
||||
<Stack gap="1em">
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns={{ sm: '1fr', md: 'repeat(2, minmax(49%, 1fr))' }}
|
||||
gridTemplateRows="minmax(0, 18em)"
|
||||
gap="1em"
|
||||
>
|
||||
<ConfigurationUISection />
|
||||
<LabelsSection />
|
||||
<AuthenticationSection />
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
|
||||
|
@ -15,11 +15,16 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useState, memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Pin as GotoIcon, RefreshDouble, Cancel } from 'iconoir-react'
|
||||
import GotoIcon from 'iconoir-react/dist/Pin'
|
||||
import RefreshDouble from 'iconoir-react/dist/RefreshDouble'
|
||||
import Cancel from 'iconoir-react/dist/Cancel'
|
||||
import { Typography, Box, Stack, Chip } from '@mui/material'
|
||||
import { Row } from 'react-table'
|
||||
|
||||
import { useLazyGetVmQuery } from 'client/features/OneApi/vm'
|
||||
import {
|
||||
useLazyGetVmQuery,
|
||||
useUpdateUserTemplateMutation,
|
||||
} from 'client/features/OneApi/vm'
|
||||
import { VmsTable } from 'client/components/Tables'
|
||||
import VmActions from 'client/components/Tables/Vms/actions'
|
||||
import VmTabs from 'client/components/Tabs/Vm'
|
||||
@ -48,6 +53,7 @@ function VirtualMachines() {
|
||||
<VmsTable
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
globalActions={actions}
|
||||
useUpdateMutation={useUpdateUserTemplateMutation}
|
||||
/>
|
||||
|
||||
{hasSelectedRows && (
|
||||
|
@ -19,7 +19,10 @@ import { Pin as GotoIcon, RefreshDouble, Cancel } from 'iconoir-react'
|
||||
import { Typography, Box, Stack, Chip } from '@mui/material'
|
||||
import { Row } from 'react-table'
|
||||
|
||||
import { useLazyGetTemplateQuery } from 'client/features/OneApi/vmTemplate'
|
||||
import {
|
||||
useLazyGetTemplateQuery,
|
||||
useUpdateTemplateMutation,
|
||||
} from 'client/features/OneApi/vmTemplate'
|
||||
import { VmTemplatesTable } from 'client/components/Tables'
|
||||
import VmTemplateActions from 'client/components/Tables/VmTemplates/actions'
|
||||
import VmTemplateTabs from 'client/components/Tabs/VmTemplate'
|
||||
@ -48,6 +51,7 @@ function VmTemplates() {
|
||||
<VmTemplatesTable
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
globalActions={actions}
|
||||
useUpdateMutation={useUpdateTemplateMutation}
|
||||
/>
|
||||
|
||||
{hasSelectedRows && (
|
||||
|
@ -22,6 +22,7 @@ import { name as authSlice, actions, logout } from 'client/features/Auth/slice'
|
||||
import groupApi from 'client/features/OneApi/group'
|
||||
import systemApi from 'client/features/OneApi/system'
|
||||
import { ResourceView } from 'client/apps/sunstone/routes'
|
||||
import { areStringEqual } from 'client/models/Helper'
|
||||
import {
|
||||
_APPS,
|
||||
RESOURCE_NAMES,
|
||||
@ -61,6 +62,15 @@ export const useAuth = () => {
|
||||
}
|
||||
)
|
||||
|
||||
const userLabels = useMemo(() => {
|
||||
const labels = user?.TEMPLATE?.LABELS?.split(',') ?? []
|
||||
|
||||
return labels
|
||||
.filter(Boolean)
|
||||
.map((label) => label.toUpperCase())
|
||||
.sort(areStringEqual({ numeric: true, ignorePunctuation: true }))
|
||||
}, [user?.TEMPLATE?.LABELS])
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
...auth,
|
||||
@ -75,6 +85,7 @@ export const useAuth = () => {
|
||||
...(user?.TEMPLATE ?? {}),
|
||||
...(user?.TEMPLATE?.FIREEDGE ?? {}),
|
||||
},
|
||||
labels: userLabels ?? [],
|
||||
isLogged:
|
||||
!!jwt &&
|
||||
!!user &&
|
||||
|
@ -20,6 +20,7 @@ import { dismissSnackbar } from 'client/features/General/actions'
|
||||
import { oneApi, ONE_RESOURCES_POOL } from 'client/features/OneApi'
|
||||
import userApi from 'client/features/OneApi/user'
|
||||
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { storage } from 'client/utils'
|
||||
import { JWT_NAME, FILTER_POOL, ONEADMIN_ID } from 'client/constants'
|
||||
|
||||
@ -88,8 +89,8 @@ const authApi = oneApi.injectEndpoints({
|
||||
}),
|
||||
changeAuthGroup: builder.mutation({
|
||||
/**
|
||||
* @param {object} data - User credentials
|
||||
* @param {string} data.group - Group id
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string} params.group - Group id
|
||||
* @returns {Promise} Response data from request
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
@ -118,6 +119,67 @@ const authApi = oneApi.injectEndpoints({
|
||||
},
|
||||
invalidatesTags: [...Object.values(restOfPool)],
|
||||
}),
|
||||
addLabel: builder.mutation({
|
||||
/**
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string} params.newLabel - Label to add
|
||||
* @returns {Promise} Response data from request
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
queryFn: async ({ newLabel } = {}, { getState, dispatch }) => {
|
||||
try {
|
||||
if (!newLabel) return { data: '' }
|
||||
|
||||
const authUser = getState().auth.user
|
||||
const currentLabels = authUser?.TEMPLATE?.LABELS?.split(',') ?? []
|
||||
const upperCaseLabels = currentLabels.map((l) => l.toUpperCase())
|
||||
const upperCaseNewLabel = newLabel.toUpperCase()
|
||||
|
||||
const exists = upperCaseLabels.some((l) => l === upperCaseNewLabel)
|
||||
if (exists) return { data: upperCaseNewLabel }
|
||||
|
||||
const newLabels = currentLabels.concat(upperCaseNewLabel).join()
|
||||
const template = jsonToXml({ LABELS: newLabels })
|
||||
const queryData = { id: authUser.ID, template, replace: 1 }
|
||||
|
||||
await dispatch(
|
||||
userApi.endpoints.updateUser.initiate(queryData)
|
||||
).unwrap()
|
||||
|
||||
return { data: upperCaseNewLabel }
|
||||
} catch (error) {
|
||||
return { error }
|
||||
}
|
||||
},
|
||||
}),
|
||||
removeLabel: builder.mutation({
|
||||
/**
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string} params.label - Label to remove
|
||||
* @returns {Promise} Response data from request
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
queryFn: async ({ label } = {}, { getState, dispatch }) => {
|
||||
try {
|
||||
if (!label) return { data: '' }
|
||||
|
||||
const authUser = getState().auth.user
|
||||
const currentLabels = authUser?.TEMPLATE?.LABELS?.split(',') ?? []
|
||||
|
||||
const newLabels = currentLabels.filter((l) => l !== label).join()
|
||||
const template = jsonToXml({ LABELS: newLabels })
|
||||
const queryData = { id: authUser.ID, template, replace: 1 }
|
||||
|
||||
await dispatch(
|
||||
userApi.endpoints.updateUser.initiate(queryData)
|
||||
).unwrap()
|
||||
|
||||
return { data: label }
|
||||
} catch (error) {
|
||||
return { error }
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@ -129,6 +191,8 @@ export const {
|
||||
// Mutations
|
||||
useLoginMutation,
|
||||
useChangeAuthGroupMutation,
|
||||
useAddLabelMutation,
|
||||
useRemoveLabelMutation,
|
||||
} = authApi
|
||||
|
||||
export default authApi
|
||||
|
@ -294,3 +294,30 @@ export const updateTemplateOnDocument =
|
||||
? { ...resource.TEMPLATE.BODY, ...template }
|
||||
: template
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current user groups in the store.
|
||||
*
|
||||
* @param {object} params - Request params
|
||||
* @param {string|number} params.id - The id of the user
|
||||
* @param {string|number} params.group - The group id to update
|
||||
* @param {boolean} [remove] - Remove the group from the user
|
||||
* @returns {function(Draft):ThunkAction} - Dispatches the action
|
||||
*/
|
||||
export const updateUserGroups =
|
||||
({ id: userId, group: groupId }, remove = false) =>
|
||||
(draft) => {
|
||||
const updatePool = isUpdateOnPool(draft, userId)
|
||||
|
||||
const resource = updatePool
|
||||
? draft.find(({ ID }) => +ID === +userId)
|
||||
: draft
|
||||
|
||||
if ((updatePool && !resource) || groupId === undefined) return
|
||||
|
||||
const currentGroups = [resource.GROUPS.ID].flat()
|
||||
|
||||
resource.GROUPS.ID = remove
|
||||
? currentGroups.filter((id) => +id !== +groupId)
|
||||
: currentGroups.concat(groupId)
|
||||
}
|
||||
|
@ -173,6 +173,7 @@ const provisionApi = oneApi.injectEndpoints({
|
||||
*
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string} params.id - Provision id
|
||||
* @param {boolean} params.force - Force configure to execute
|
||||
* @returns {object} Object of document updated
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
@ -220,7 +221,7 @@ const provisionApi = oneApi.injectEndpoints({
|
||||
* @returns {object} Object of document deleted
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: ({ provision: _, ...params }) => {
|
||||
query: (params) => {
|
||||
const name = Actions.PROVISION_DELETE_RESOURCE
|
||||
const command = { name, ...Commands[name] }
|
||||
|
||||
|
@ -20,7 +20,15 @@ import {
|
||||
ONE_RESOURCES,
|
||||
ONE_RESOURCES_POOL,
|
||||
} from 'client/features/OneApi'
|
||||
import authApi from 'client/features/OneApi/auth'
|
||||
import { actions as authActions } from 'client/features/Auth/slice'
|
||||
|
||||
import {
|
||||
updateResourceOnPool,
|
||||
removeResourceOnPool,
|
||||
updateUserGroups,
|
||||
updateTemplateOnResource,
|
||||
updateOwnershipOnResource,
|
||||
} from 'client/features/OneApi/common'
|
||||
import { User } from 'client/constants'
|
||||
|
||||
const { USER } = ONE_RESOURCES
|
||||
@ -67,6 +75,28 @@ const userApi = oneApi.injectEndpoints({
|
||||
},
|
||||
transformResponse: (data) => data?.USER ?? {},
|
||||
providesTags: (_, __, { id }) => [{ type: USER, id }],
|
||||
async onQueryStarted({ id }, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data: resourceFromQuery } = await queryFulfilled
|
||||
|
||||
dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUsers',
|
||||
undefined,
|
||||
updateResourceOnPool({ id, resourceFromQuery })
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
// if the query fails, we want to remove the resource from the pool
|
||||
dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUsers',
|
||||
undefined,
|
||||
removeResourceOnPool({ id })
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
}),
|
||||
allocateUser: builder.mutation({
|
||||
/**
|
||||
@ -75,9 +105,9 @@ const userApi = oneApi.injectEndpoints({
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string} params.username - Username for the new user
|
||||
* @param {string} params.password - Password for the new user
|
||||
* @param {string} params.driver - Authentication driver for the new user.
|
||||
* @param {string} [params.driver] - Authentication driver for the new user.
|
||||
* If it is an empty string, then the default 'core' is used
|
||||
* @param {string[]} params.group - array of Group IDs.
|
||||
* @param {string[]} [params.group] - array of Group IDs.
|
||||
* **The first ID will be used as the main group.**
|
||||
* This array can be empty, in which case the default group will be used
|
||||
* @returns {number} The allocated User id
|
||||
@ -89,7 +119,6 @@ const userApi = oneApi.injectEndpoints({
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: [USER_POOL],
|
||||
}),
|
||||
updateUser: builder.mutation({
|
||||
/**
|
||||
@ -112,17 +141,37 @@ const userApi = oneApi.injectEndpoints({
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: USER, id }],
|
||||
async onQueryStarted({ id }, { queryFulfilled, dispatch, getState }) {
|
||||
async onQueryStarted(params, { dispatch, queryFulfilled, getState }) {
|
||||
try {
|
||||
await queryFulfilled
|
||||
|
||||
if (+id === +getState().auth.user.ID) {
|
||||
await dispatch(
|
||||
authApi.endpoints.getAuthUser.initiate(undefined, {
|
||||
forceRefetch: true,
|
||||
})
|
||||
const patchUser = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUser',
|
||||
{ id: params.id },
|
||||
updateTemplateOnResource(params)
|
||||
)
|
||||
)
|
||||
|
||||
const patchUsers = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUsers',
|
||||
undefined,
|
||||
updateTemplateOnResource(params)
|
||||
)
|
||||
)
|
||||
|
||||
const authUser = getState().auth.user
|
||||
|
||||
if (+authUser?.ID === +params.id) {
|
||||
// optimistic update of the auth user
|
||||
const cloneAuthUser = { ...authUser }
|
||||
updateTemplateOnResource(params)(cloneAuthUser)
|
||||
dispatch(authActions.changeAuthUser({ ...cloneAuthUser }))
|
||||
}
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchUser.undo()
|
||||
patchUsers.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
@ -141,7 +190,7 @@ const userApi = oneApi.injectEndpoints({
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: USER, id }, USER_POOL],
|
||||
invalidatesTags: [USER_POOL],
|
||||
}),
|
||||
changePassword: builder.mutation({
|
||||
/**
|
||||
@ -183,7 +232,7 @@ const userApi = oneApi.injectEndpoints({
|
||||
}),
|
||||
changeGroup: builder.mutation({
|
||||
/**
|
||||
* Changes the group of the given user.
|
||||
* Changes the User's primary group of the given user.
|
||||
*
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string|number} params.id - User id
|
||||
@ -198,26 +247,32 @@ const userApi = oneApi.injectEndpoints({
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: USER, id }],
|
||||
async onQueryStarted({ id, group }, { dispatch, queryFulfilled }) {
|
||||
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled
|
||||
|
||||
dispatch(
|
||||
userApi.util.updateQueryData('getUsers', undefined, (draft) => {
|
||||
const user = draft.find(({ ID }) => +ID === +id)
|
||||
user && (user.GID = group)
|
||||
})
|
||||
const patchUser = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUser',
|
||||
{ id: params.id },
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
dispatch(
|
||||
userApi.util.updateQueryData('getUser', id, (draftUser) => {
|
||||
draftUser.GID = group
|
||||
})
|
||||
const patchUsers = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUsers',
|
||||
undefined,
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchUser.undo()
|
||||
patchUsers.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
addToGroup: builder.mutation({
|
||||
addGroup: builder.mutation({
|
||||
/**
|
||||
* Adds the User to a secondary group.
|
||||
*
|
||||
@ -234,6 +289,39 @@ const userApi = oneApi.injectEndpoints({
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: USER, id }, USER_POOL],
|
||||
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const patchUser = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUser',
|
||||
{ id: params.id },
|
||||
updateUserGroups(params)
|
||||
)
|
||||
)
|
||||
|
||||
const patchUsers = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUsers',
|
||||
undefined,
|
||||
updateUserGroups(params)
|
||||
)
|
||||
)
|
||||
|
||||
const authUser = getState().auth.user
|
||||
|
||||
if (+authUser?.ID === +params.id) {
|
||||
// optimistic update of the auth user
|
||||
const cloneAuthUser = { ...authUser }
|
||||
updateUserGroups(params)(cloneAuthUser)
|
||||
dispatch(authActions.changeAuthUser({ ...cloneAuthUser }))
|
||||
}
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchUser.undo()
|
||||
patchUsers.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
removeFromGroup: builder.mutation({
|
||||
/**
|
||||
@ -252,6 +340,39 @@ const userApi = oneApi.injectEndpoints({
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: USER, id }, USER_POOL],
|
||||
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const patchUser = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUser',
|
||||
{ id: params.id },
|
||||
updateUserGroups(params, true)
|
||||
)
|
||||
)
|
||||
|
||||
const patchUsers = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUsers',
|
||||
undefined,
|
||||
updateUserGroups(params, true)
|
||||
)
|
||||
)
|
||||
|
||||
const authUser = getState().auth.user
|
||||
|
||||
if (+authUser?.ID === +params.id) {
|
||||
// optimistic update of the auth user
|
||||
const cloneAuthUser = { ...authUser }
|
||||
updateUserGroups(params, true)(cloneAuthUser)
|
||||
dispatch(authActions.changeAuthUser({ ...cloneAuthUser }))
|
||||
}
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchUser.undo()
|
||||
patchUsers.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
enableUser: builder.mutation({
|
||||
/**
|
||||
@ -352,7 +473,7 @@ export const {
|
||||
useChangePasswordMutation,
|
||||
useChangeAuthDriverMutation,
|
||||
useChangeGroupMutation,
|
||||
useAddToGroupMutation,
|
||||
useAddGroupMutation,
|
||||
useRemoveFromGroupMutation,
|
||||
useEnableUserMutation,
|
||||
useDisableUserMutation,
|
||||
|
@ -315,7 +315,7 @@ const vmApi = oneApi.injectEndpoints({
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: [VM_POOL],
|
||||
invalidatesTags: (_, __, { id }) => [{ type: VM, id }, VM_POOL],
|
||||
}),
|
||||
actionVm: builder.mutation({
|
||||
/**
|
||||
|
@ -13,50 +13,80 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useState, useMemo, useCallback } from 'react'
|
||||
|
||||
import { debounce } from '@mui/material'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
/**
|
||||
* @typedef {object} useSearchHook
|
||||
* @property {string} query - Search term
|
||||
* @property {Array} result - Result of the search
|
||||
* @property {Function} handleChange - Function to handle the change event
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hook to manage a search in a list.
|
||||
*
|
||||
* @param {object} params - Search parameters
|
||||
* @param {Array} params.list - List of elements
|
||||
* @param {Fuse.IFuseOptions} params.listOptions - Search options
|
||||
* @returns {{
|
||||
* query: string,
|
||||
* result: Array,
|
||||
* handleChange: Function
|
||||
* }} - Returns information about the search
|
||||
* @param {Fuse.IFuseOptions} [params.listOptions] - Search options
|
||||
* @param {number} [params.wait] - Wait a certain amount of time before searching again. By default 1 second
|
||||
* @param {boolean} [params.condition] - Search if the condition is true
|
||||
* @returns {useSearchHook} - Returns information about the search
|
||||
*/
|
||||
const useSearch = ({ list, listOptions }) => {
|
||||
const [query, setQuery] = useState('')
|
||||
const useSearch = ({ list, listOptions, wait, condition }) => {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [result, setResult] = useState(undefined)
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, wait, condition)
|
||||
|
||||
const listFuse = useMemo(
|
||||
() =>
|
||||
new Fuse(list, listOptions, Fuse.createIndex(listOptions?.keys, list)),
|
||||
[list, listOptions]
|
||||
)
|
||||
const listFuse = useMemo(() => {
|
||||
const indexed = listOptions?.keys
|
||||
? Fuse.createIndex(listOptions.keys, list)
|
||||
: undefined
|
||||
|
||||
const debounceResult = useCallback(
|
||||
debounce((value) => {
|
||||
const search = listFuse.search(value)?.map(({ item }) => item)
|
||||
return new Fuse(list, listOptions, indexed)
|
||||
}, [list, listOptions])
|
||||
|
||||
setResult(value ? search : undefined)
|
||||
}, 1000),
|
||||
[list]
|
||||
)
|
||||
useEffect(() => {
|
||||
const search = debouncedSearchTerm
|
||||
? listFuse.search(debouncedSearchTerm).map(({ item }) => item)
|
||||
: undefined
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value: nextValue } = event?.target
|
||||
setResult(search)
|
||||
}, [debouncedSearchTerm])
|
||||
|
||||
setQuery(nextValue)
|
||||
debounceResult(nextValue)
|
||||
return {
|
||||
query: searchTerm,
|
||||
result: result ?? list,
|
||||
handleChange: (evt) => setSearchTerm(evt?.target?.value),
|
||||
}
|
||||
}
|
||||
|
||||
return { query, result, handleChange }
|
||||
/**
|
||||
* This hook allows you to debounce any fast changing value.
|
||||
* The debounced value will only reflect the latest value when
|
||||
* the useDebounce hook has not been called for the specified time period.
|
||||
*
|
||||
* @param {string} value - Value to debounce
|
||||
* @param {number} [delay] - Delay in milliseconds
|
||||
* @param {boolean} [condition] - Condition to check if the value should be debounced
|
||||
* @returns {string} Debounced value
|
||||
*/
|
||||
const useDebounce = (value, delay = 1000, condition = true) => {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value)
|
||||
|
||||
useEffect(() => {
|
||||
// Update debounced value after delay
|
||||
const handler = setTimeout(() => {
|
||||
condition && setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||
return () => {
|
||||
clearTimeout(handler)
|
||||
}
|
||||
}, [value, delay, condition])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
export default useSearch
|
||||
|
@ -27,7 +27,8 @@ import {
|
||||
Permission,
|
||||
UserInputObject,
|
||||
USER_INPUT_TYPES,
|
||||
SERVER_CONFIG,
|
||||
CURRENCY,
|
||||
LOCALE,
|
||||
} from 'client/constants'
|
||||
|
||||
/**
|
||||
@ -90,12 +91,9 @@ export const stringToBoolean = (str) =>
|
||||
*/
|
||||
export const formatNumberByCurrency = (number, options) => {
|
||||
try {
|
||||
const currency = SERVER_CONFIG?.currency ?? 'EUR'
|
||||
const locale = SERVER_CONFIG?.lang?.replace('_', '-') ?? undefined
|
||||
|
||||
return Intl.NumberFormat(locale, {
|
||||
return Intl.NumberFormat(LOCALE, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
currency: CURRENCY,
|
||||
currencyDisplay: 'narrowSymbol',
|
||||
notation: 'compact',
|
||||
compactDisplay: 'long',
|
||||
@ -107,6 +105,28 @@ export const formatNumberByCurrency = (number, options) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to compare two values.
|
||||
*
|
||||
* @param {Intl.CollatorOptions} options - Options to compare the values
|
||||
* @returns {function(string, string)} - Function to compare two strings
|
||||
* Negative when the referenceStr occurs before compareString
|
||||
* Positive when the referenceStr occurs after compareString
|
||||
* Returns 0 if they are equivalent
|
||||
*/
|
||||
export const areStringEqual = (options) => (a, b) => {
|
||||
try {
|
||||
const collator = new Intl.Collator(LOCALE, {
|
||||
sensitivity: 'base',
|
||||
...options,
|
||||
})
|
||||
|
||||
return collator.compare(a, b)
|
||||
} catch {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the given value is an instance of Date.
|
||||
*
|
||||
|
@ -286,6 +286,22 @@ const createAppTheme = (appTheme, mode = SCHEMES.DARK) => {
|
||||
fieldset: { border: 'none' },
|
||||
},
|
||||
},
|
||||
MuiTypography: {
|
||||
variants: [
|
||||
{
|
||||
props: { variant: 'underline' },
|
||||
style: {
|
||||
padding: '0 1em 0.2em 0.5em',
|
||||
borderBottom: `2px solid ${secondary.main}`,
|
||||
// subtitle1 variant is used for the underline
|
||||
fontSize: defaultTheme.typography.pxToRem(18),
|
||||
lineHeight: 24 / 18,
|
||||
letterSpacing: 0,
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
MuiPaper: {
|
||||
defaultProps: {
|
||||
elevation: 0,
|
||||
|
@ -90,27 +90,30 @@ const setup = (
|
||||
userData = {},
|
||||
oneConnection = defaultEmptyFunction
|
||||
) => {
|
||||
const { user, password } = userData
|
||||
if (!(user && password)) {
|
||||
next()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const { token } = params
|
||||
const oneConnect = oneConnection()
|
||||
getUserInfoAuthenticated(oneConnect, next, (user) => {
|
||||
const oneConnect = oneConnection(user, password)
|
||||
getUserInfoAuthenticated(oneConnect, next, (data) => {
|
||||
if (
|
||||
user &&
|
||||
user.USER &&
|
||||
user.USER.ID &&
|
||||
user.USER.TEMPLATE &&
|
||||
user.USER.TEMPLATE.SUNSTONE &&
|
||||
user.USER.TEMPLATE.SUNSTONE[default2FAOpennebulaTmpVar] &&
|
||||
Number.isInteger(parseInt(data?.USER?.ID, 10)) &&
|
||||
data?.USER?.TEMPLATE?.SUNSTONE?.[default2FAOpennebulaTmpVar] &&
|
||||
token
|
||||
) {
|
||||
const sunstone = user.USER.TEMPLATE.SUNSTONE
|
||||
const sunstone = data.USER.TEMPLATE.SUNSTONE
|
||||
const secret = sunstone[default2FAOpennebulaTmpVar]
|
||||
if (check2Fa(secret, token)) {
|
||||
oneConnect({
|
||||
action: Actions.USER_UPDATE,
|
||||
parameters: [
|
||||
parseInt(user.USER.ID, 10),
|
||||
parseInt(data.USER.ID, 10),
|
||||
generateNewResourceTemplate(
|
||||
user.USER.TEMPLATE.SUNSTONE || {},
|
||||
data.USER.TEMPLATE.SUNSTONE || {},
|
||||
{ [default2FAOpennebulaVar]: secret },
|
||||
[default2FAOpennebulaTmpVar]
|
||||
),
|
||||
@ -157,6 +160,13 @@ const qr = (
|
||||
userData = {},
|
||||
oneConnection = defaultEmptyFunction
|
||||
) => {
|
||||
const { user, password } = userData
|
||||
if (!(user && password)) {
|
||||
next()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const secret = speakeasy.generateSecret({
|
||||
length: 10,
|
||||
name: twoFactorAuthIssuer,
|
||||
@ -168,15 +178,15 @@ const qr = (
|
||||
res.locals.httpCode = httpResponse(internalServerError)
|
||||
next()
|
||||
} else {
|
||||
const oneConnect = oneConnection()
|
||||
getUserInfoAuthenticated(oneConnect, next, (user) => {
|
||||
if (user && user.USER && user.USER.ID && user.USER.TEMPLATE) {
|
||||
const oneConnect = oneConnection(user, password)
|
||||
getUserInfoAuthenticated(oneConnect, next, (data) => {
|
||||
if (data?.USER?.ID && data?.USER?.TEMPLATE) {
|
||||
oneConnect({
|
||||
action: Actions.USER_UPDATE,
|
||||
parameters: [
|
||||
parseInt(user.USER.ID, 10),
|
||||
parseInt(data.USER.ID, 10),
|
||||
generateNewResourceTemplate(
|
||||
user.USER.TEMPLATE.SUNSTONE || {},
|
||||
data.USER.TEMPLATE.SUNSTONE || {},
|
||||
{ [default2FAOpennebulaTmpVar]: base32 },
|
||||
[default2FAOpennebulaVar]
|
||||
),
|
||||
@ -228,20 +238,21 @@ const del = (
|
||||
userData = {},
|
||||
oneConnection = defaultEmptyFunction
|
||||
) => {
|
||||
const oneConnect = oneConnection()
|
||||
getUserInfoAuthenticated(oneConnect, next, (user) => {
|
||||
if (
|
||||
user &&
|
||||
user.USER &&
|
||||
user.USER.ID &&
|
||||
user.USER.TEMPLATE &&
|
||||
user.USER.TEMPLATE.SUNSTONE
|
||||
) {
|
||||
const { user, password } = userData
|
||||
if (!(user && password)) {
|
||||
next()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const oneConnect = oneConnection(user, password)
|
||||
getUserInfoAuthenticated(oneConnect, next, (data) => {
|
||||
if (data?.USER?.TEMPLATE?.SUNSTONE) {
|
||||
oneConnect({
|
||||
action: Actions.USER_UPDATE,
|
||||
parameters: [
|
||||
parseInt(user.USER.ID, 10),
|
||||
generateNewResourceTemplate(user.USER.TEMPLATE.SUNSTONE || {}, {}, [
|
||||
parseInt(data.USER.ID, 10),
|
||||
generateNewResourceTemplate(data.USER.TEMPLATE.SUNSTONE || {}, {}, [
|
||||
default2FAOpennebulaTmpVar,
|
||||
default2FAOpennebulaVar,
|
||||
]),
|
||||
|
@ -60,7 +60,7 @@ const loginUser = (
|
||||
if (value && value.USER && !err) {
|
||||
success(value)
|
||||
} else {
|
||||
error()
|
||||
error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,11 +103,14 @@ const auth = (
|
||||
}
|
||||
|
||||
/**
|
||||
* Run if no have information.
|
||||
* Catch error login.
|
||||
*
|
||||
* @param {string} err - error.
|
||||
*/
|
||||
const error = () => {
|
||||
updaterResponse(new Map(unauthorized).toObject())
|
||||
writeInLogger(unauthorized)
|
||||
const error = (err) => {
|
||||
const httpCodeError = err ? internalServerError : unauthorized
|
||||
updaterResponse(new Map(httpCodeError).toObject())
|
||||
writeInLogger(httpCodeError)
|
||||
next()
|
||||
}
|
||||
|
||||
|
@ -252,17 +252,13 @@ const updaterResponse = (code) => {
|
||||
*/
|
||||
const validate2faAuthentication = (informationUser) => {
|
||||
let rtn = false
|
||||
if (
|
||||
informationUser.TEMPLATE &&
|
||||
informationUser.TEMPLATE.SUNSTONE &&
|
||||
informationUser.TEMPLATE.SUNSTONE[default2FAOpennebulaVar]
|
||||
) {
|
||||
if (informationUser?.TEMPLATE?.SUNSTONE?.[default2FAOpennebulaVar]) {
|
||||
/*********************************************************
|
||||
* Validate 2FA
|
||||
*********************************************************/
|
||||
|
||||
if (tfatoken.length <= 0) {
|
||||
updaterResponse(httpResponse(accepted))
|
||||
updaterResponse(httpResponse(accepted, { id: informationUser?.ID }))
|
||||
} else {
|
||||
const secret = informationUser.TEMPLATE.SUNSTONE[default2FAOpennebulaVar]
|
||||
if (!check2Fa(secret, tfatoken)) {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,295 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
|
||||
* *
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
|
||||
* not use this file except in compliance with the License. You may obtain *
|
||||
* a copy of the License at *
|
||||
* *
|
||||
* http://www.apache.org/licenses/LICENSE-2.0 *
|
||||
* *
|
||||
* Unless required by applicable law or agreed to in writing, software *
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, *
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
const btoa = require('btoa')
|
||||
const { parse } = require('yaml')
|
||||
const { v4 } = require('uuid')
|
||||
const { DateTime } = require('luxon')
|
||||
const { publish } = require('server/utils/server')
|
||||
const {
|
||||
httpResponse,
|
||||
existsFile,
|
||||
rotateBySize,
|
||||
executeCommand,
|
||||
executeCommandAsync,
|
||||
} = require('server/utils/server')
|
||||
const { defaults, httpCodes } = require('server/utils/constants')
|
||||
const {
|
||||
findRecursiveFolder,
|
||||
getSpecificConfig,
|
||||
} = require('server/routes/api/oneprovision/utils')
|
||||
|
||||
const {
|
||||
defaultCommandProvision,
|
||||
defaultEmptyFunction,
|
||||
defaultRegexpStartJSON,
|
||||
defaultRegexpEndJSON,
|
||||
defaultRegexpSplitLine,
|
||||
defaultSizeRotate,
|
||||
} = defaults
|
||||
const { ok, internalServerError } = httpCodes
|
||||
const relName = 'provision-mapping'
|
||||
const ext = 'yml'
|
||||
const logFile = {
|
||||
name: 'stdouterr',
|
||||
ext: 'log',
|
||||
}
|
||||
const appendError = '.ERROR'
|
||||
|
||||
/**
|
||||
* Execute command Async and emit in WS.
|
||||
*
|
||||
* @param {string} command - command to execute
|
||||
* @param {object} actions - external functions when command emit in stderr, stdout and finalize
|
||||
* @param {Function} actions.err - emit when have stderr
|
||||
* @param {Function} actions.out - emit when have stdout
|
||||
* @param {Function} actions.close - emit when finalize
|
||||
* @param {object} dataForLog - data
|
||||
* @param {number} dataForLog.id - data id
|
||||
* @param {string} dataForLog.command - data command
|
||||
* @returns {boolean} check if emmit data
|
||||
*/
|
||||
const executeWithEmit = (command = [], actions = {}, dataForLog = {}) => {
|
||||
if (
|
||||
!(
|
||||
command &&
|
||||
Array.isArray(command) &&
|
||||
command.length > 0 &&
|
||||
actions &&
|
||||
dataForLog
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const { err: externalErr, out: externalOut, close: externalClose } = actions
|
||||
const err =
|
||||
externalErr && typeof externalErr === 'function'
|
||||
? externalErr
|
||||
: defaultEmptyFunction
|
||||
const out =
|
||||
externalOut && typeof externalOut === 'function'
|
||||
? externalOut
|
||||
: defaultEmptyFunction
|
||||
const close =
|
||||
externalClose && typeof externalClose === 'function'
|
||||
? actions.close
|
||||
: defaultEmptyFunction
|
||||
|
||||
// data for log
|
||||
const id = (dataForLog && dataForLog.id) || ''
|
||||
const commandName = (dataForLog && dataForLog.command) || ''
|
||||
|
||||
let lastLine = ''
|
||||
const uuid = v4()
|
||||
|
||||
let pendingMessages = ''
|
||||
|
||||
/**
|
||||
* Emit data of command.
|
||||
*
|
||||
* @param {string} message - line of command CLI
|
||||
* @param {Function} callback - function when recieve a information
|
||||
*/
|
||||
const emit = (message, callback = defaultEmptyFunction) => {
|
||||
/**
|
||||
* Publisher data to WS.
|
||||
*
|
||||
* @param {string} line - command CLI line
|
||||
*/
|
||||
const publisher = (line = '') => {
|
||||
const resposeData = callback(line, uuid) || {
|
||||
id,
|
||||
data: line,
|
||||
command: commandName,
|
||||
commandId: uuid,
|
||||
}
|
||||
publish(defaultCommandProvision, resposeData)
|
||||
}
|
||||
|
||||
message
|
||||
.toString()
|
||||
.split(defaultRegexpSplitLine)
|
||||
.forEach((line) => {
|
||||
if (line) {
|
||||
if (
|
||||
(defaultRegexpStartJSON.test(line) &&
|
||||
defaultRegexpEndJSON.test(line)) ||
|
||||
(!defaultRegexpStartJSON.test(line) &&
|
||||
!defaultRegexpEndJSON.test(line) &&
|
||||
pendingMessages.length === 0)
|
||||
) {
|
||||
lastLine = line
|
||||
publisher(lastLine)
|
||||
} else if (
|
||||
(defaultRegexpStartJSON.test(line) &&
|
||||
!defaultRegexpEndJSON.test(line)) ||
|
||||
(!defaultRegexpStartJSON.test(line) &&
|
||||
!defaultRegexpEndJSON.test(line) &&
|
||||
pendingMessages.length > 0)
|
||||
) {
|
||||
pendingMessages += line
|
||||
} else {
|
||||
lastLine = pendingMessages + line
|
||||
publisher(lastLine)
|
||||
pendingMessages = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
executeCommandAsync(
|
||||
defaultCommandProvision,
|
||||
command,
|
||||
getSpecificConfig('oneprovision_prepend_command'),
|
||||
{
|
||||
err: (message) => {
|
||||
emit(message, err)
|
||||
},
|
||||
out: (message) => {
|
||||
emit(message, out)
|
||||
},
|
||||
close: (success) => {
|
||||
close(success, lastLine)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Find log data.
|
||||
*
|
||||
* @param {string} id - id of provision
|
||||
* @param {boolean} fullPath - if need return the path of log
|
||||
* @returns {object} data of log
|
||||
*/
|
||||
const logData = (id, fullPath = false) => {
|
||||
let rtn = false
|
||||
if (!Number.isInteger(parseInt(id, 10))) {
|
||||
return rtn
|
||||
}
|
||||
const basePath = `${global.paths.CPI}/provision`
|
||||
const relFile = `${basePath}/${relName}`
|
||||
const relFileYML = `${relFile}.${ext}`
|
||||
const find = findRecursiveFolder(basePath, id)
|
||||
|
||||
/**
|
||||
* Found log.
|
||||
*
|
||||
* @param {string} path - path of log
|
||||
* @param {string} uuid - uuid of log
|
||||
*/
|
||||
const rtnFound = (path = '', uuid) => {
|
||||
if (!path) {
|
||||
return
|
||||
}
|
||||
|
||||
const stringPath = `${path}/${logFile.name}.${logFile.ext}`
|
||||
existsFile(
|
||||
stringPath,
|
||||
(filedata) => {
|
||||
rotateBySize(stringPath, defaultSizeRotate)
|
||||
rtn = { uuid, log: filedata.split(defaultRegexpSplitLine) }
|
||||
if (fullPath) {
|
||||
rtn.fullPath = stringPath
|
||||
}
|
||||
},
|
||||
defaultEmptyFunction
|
||||
)
|
||||
}
|
||||
|
||||
if (find) {
|
||||
rtnFound(find)
|
||||
} else {
|
||||
// Temporal provision
|
||||
existsFile(
|
||||
relFileYML,
|
||||
(filedata) => {
|
||||
const fileData = parse(filedata) || {}
|
||||
if (!fileData[id]) {
|
||||
return
|
||||
}
|
||||
|
||||
const findPending = findRecursiveFolder(basePath, fileData[id])
|
||||
if (findPending) {
|
||||
rtnFound(findPending, fileData[id])
|
||||
} else {
|
||||
const findError = findRecursiveFolder(
|
||||
basePath,
|
||||
fileData[id] + appendError
|
||||
)
|
||||
findError && rtnFound(findError, fileData[id])
|
||||
}
|
||||
},
|
||||
defaultEmptyFunction
|
||||
)
|
||||
}
|
||||
|
||||
return rtn
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Command sync and return http response.
|
||||
*
|
||||
* @param {any[]} params - params for command.
|
||||
* @returns {object} httpResponse
|
||||
*/
|
||||
const addResourceSync = (params) => {
|
||||
if (!(params && Array.isArray(params))) {
|
||||
return
|
||||
}
|
||||
|
||||
const executedCommand = executeCommand(
|
||||
defaultCommandProvision,
|
||||
params,
|
||||
getSpecificConfig('oneprovision_prepend_command')
|
||||
)
|
||||
try {
|
||||
const response = executedCommand.success ? ok : internalServerError
|
||||
|
||||
return httpResponse(
|
||||
response,
|
||||
executedCommand.data ? JSON.parse(executedCommand.data) : params.id
|
||||
)
|
||||
} catch (error) {
|
||||
return httpResponse(internalServerError, '', executedCommand.data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executing line for provision logs.
|
||||
*
|
||||
* @param {string} message - message to log
|
||||
* @returns {string} line message stringify
|
||||
*/
|
||||
const executingMessage = (message = '') =>
|
||||
JSON.stringify({
|
||||
timestamp: DateTime.now().toFormat('yyyy-MM-dd HH:mm:ss ZZZ'),
|
||||
severity: 'DEBUG',
|
||||
message: btoa(message),
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
executeWithEmit,
|
||||
logData,
|
||||
addResourceSync,
|
||||
executingMessage,
|
||||
relName,
|
||||
ext,
|
||||
logFile,
|
||||
appendError,
|
||||
}
|
@ -127,6 +127,9 @@ module.exports = {
|
||||
id: {
|
||||
from: resource,
|
||||
},
|
||||
force: {
|
||||
from: postBody,
|
||||
},
|
||||
},
|
||||
},
|
||||
[PROVISION_GET_RESOURCE]: {
|
||||
@ -140,7 +143,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
[PROVISION_DELETE_RESOURCE]: {
|
||||
path: `${basepath}/resource/:resource/:id`,
|
||||
path: `${basepath}/resource/:resource/:id/:provision`,
|
||||
httpMethod: DELETE,
|
||||
auth: true,
|
||||
params: {
|
||||
@ -150,6 +153,9 @@ module.exports = {
|
||||
id: {
|
||||
from: resource,
|
||||
},
|
||||
provision: {
|
||||
from: resource,
|
||||
},
|
||||
},
|
||||
},
|
||||
[PROVISION_HOST_ACTION]: {
|
||||
|
@ -59,35 +59,36 @@ const executeWorker = ({
|
||||
next,
|
||||
res,
|
||||
}) => {
|
||||
if (user && password && rpc && command && paramsCommand) {
|
||||
const worker = useWorker()
|
||||
worker.onmessage = function (result) {
|
||||
worker.terminate()
|
||||
const err = result && result.data && result.data.err
|
||||
const value = result && result.data && result.data.value
|
||||
writeInLogger([command, paramsCommand, JSON.stringify(value)], {
|
||||
format: 'worker: %s, [%s]: %s',
|
||||
level: 2,
|
||||
})
|
||||
if (!err) {
|
||||
fillResourceforHookConnection(user, command, paramsCommand)
|
||||
res.locals.httpCode = parseReturnWorker(value)
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
worker.postMessage({
|
||||
type: 'xmlrpc',
|
||||
config: {
|
||||
globalState: (global && global.paths) || {},
|
||||
user,
|
||||
password,
|
||||
rpc,
|
||||
command,
|
||||
paramsCommand,
|
||||
},
|
||||
})
|
||||
if (!(user && password && rpc && command && paramsCommand)) {
|
||||
return
|
||||
}
|
||||
const worker = useWorker()
|
||||
worker.onmessage = function (result) {
|
||||
worker.terminate()
|
||||
const err = result && result.data && result.data.err
|
||||
const value = result && result.data && result.data.value
|
||||
writeInLogger([command, paramsCommand, JSON.stringify(value)], {
|
||||
format: 'worker: %s, [%s]: %s',
|
||||
level: 2,
|
||||
})
|
||||
if (!err) {
|
||||
fillResourceforHookConnection(user, command, paramsCommand)
|
||||
res.locals.httpCode = parseReturnWorker(value)
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
worker.postMessage({
|
||||
type: 'xmlrpc',
|
||||
config: {
|
||||
globalState: (global && global.paths) || {},
|
||||
user,
|
||||
password,
|
||||
rpc,
|
||||
command,
|
||||
paramsCommand,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,22 +101,33 @@ const executeWorker = ({
|
||||
*/
|
||||
const getCommandParams = (config) => {
|
||||
const { params, serverDataSource } = config
|
||||
if (params && serverDataSource) {
|
||||
return Object.entries(params).map(([key, value]) => {
|
||||
if (key && value && value.from && typeof value.default !== 'undefined') {
|
||||
// `value == null` checks against undefined and null
|
||||
return serverDataSource[value.from] &&
|
||||
serverDataSource[value.from][key] != null
|
||||
? upcast.to(
|
||||
serverDataSource[value.from][key],
|
||||
upcast.type(value.default)
|
||||
)
|
||||
: value.default
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
if (!(params && serverDataSource)) {
|
||||
return
|
||||
}
|
||||
|
||||
return Object.entries(params).map(([key, value]) => {
|
||||
if (!(key && value && value.from && typeof value.default !== 'undefined')) {
|
||||
return ''
|
||||
}
|
||||
// `value == null` checks against undefined and null
|
||||
if (
|
||||
serverDataSource[value.from] &&
|
||||
serverDataSource[value.from][key] != null
|
||||
) {
|
||||
const upcastedData = upcast.to(
|
||||
serverDataSource[value.from][key],
|
||||
upcast.type(value.default)
|
||||
)
|
||||
|
||||
return value.arrayDefault !== undefined && Array.isArray(upcastedData)
|
||||
? upcastedData.map((internalValue) =>
|
||||
upcast.to(internalValue, upcast.type(value.arrayDefault))
|
||||
)
|
||||
: upcastedData
|
||||
} else {
|
||||
return value.default
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,7 +61,7 @@ module.exports = {
|
||||
params: {
|
||||
username: {
|
||||
from: postBody,
|
||||
default: 0,
|
||||
default: '',
|
||||
},
|
||||
password: {
|
||||
from: postBody,
|
||||
@ -74,6 +74,7 @@ module.exports = {
|
||||
group: {
|
||||
from: postBody,
|
||||
default: [],
|
||||
arrayDefault: 0, // this is for the upcast of the internal values of the array
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -57,6 +57,7 @@ const defaults = {
|
||||
defaultRegexID: /^ID: (?<id>\d+)/,
|
||||
defaultRegexpEndJSON: /}$/,
|
||||
defaultRegexpSplitLine: /\r|\n/,
|
||||
defaultSizeRotate: '100k',
|
||||
defaultAppName: appName,
|
||||
defaultConfigErrorMessage: {
|
||||
color: 'red',
|
||||
|
@ -36,14 +36,17 @@ const {
|
||||
readdirSync,
|
||||
statSync,
|
||||
removeSync,
|
||||
moveSync,
|
||||
ensureFileSync,
|
||||
} = require('fs-extra')
|
||||
const { spawnSync, spawn } = require('child_process')
|
||||
const events = require('events')
|
||||
const { DateTime } = require('luxon')
|
||||
const { request: axios } = require('axios')
|
||||
const { defaults, httpCodes } = require('server/utils/constants')
|
||||
const { messageTerminal } = require('server/utils/general')
|
||||
const { validateAuth } = require('server/utils/jwt')
|
||||
const { writeInLogger } = require('server/utils/logger')
|
||||
const { request: axios } = require('axios')
|
||||
|
||||
const eventsEmitter = new events.EventEmitter()
|
||||
const {
|
||||
@ -304,6 +307,48 @@ const decrypt = (data = '', decryptKey = '', iv = '') => {
|
||||
return rtn
|
||||
}
|
||||
|
||||
const getSize = (limit) => {
|
||||
const size = limit?.toLowerCase?.()?.match(/^((?:0\.)?\d+)([kmg])$/)
|
||||
const limitNumber = parseInt(limit, 10)
|
||||
if (size) {
|
||||
switch (size[2]) {
|
||||
case 'k':
|
||||
return size[1] * 1024
|
||||
case 'm':
|
||||
return size[1] * 1024 ** 2
|
||||
case 'g':
|
||||
return size[1] * 1024 ** 3
|
||||
}
|
||||
} else if (Number.isInteger(limitNumber)) {
|
||||
return limitNumber
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate file by size.
|
||||
*
|
||||
*
|
||||
* @param {string} filepath - file path
|
||||
* @param {number} limit - size to rotate
|
||||
*/
|
||||
const rotateBySize = (filepath = '', limit) => {
|
||||
try {
|
||||
const fileStats = statSync(filepath)
|
||||
if (fileStats.size >= getSize(limit)) {
|
||||
moveSync(filepath, `${filepath}.${DateTime.now().toSeconds()}`)
|
||||
ensureFileSync(filepath)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorData = (error && error.message) || ''
|
||||
writeInLogger(errorData)
|
||||
messageTerminal({
|
||||
color: 'red',
|
||||
message: 'Error: %s',
|
||||
error: errorData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file exist.
|
||||
*
|
||||
@ -1005,4 +1050,5 @@ module.exports = {
|
||||
publish,
|
||||
subscriber,
|
||||
executeRequest,
|
||||
rotateBySize,
|
||||
}
|
||||
|
@ -41,6 +41,9 @@
|
||||
# Defaults
|
||||
################################################################################
|
||||
|
||||
# Default retries in case of aborting call due to authentication issue
|
||||
:retries: 5
|
||||
|
||||
# Default cooldown period after a scale operation, in seconds
|
||||
:default_cooldown: 300
|
||||
|
||||
|
@ -59,10 +59,11 @@ class ServiceLCM
|
||||
:release_cb
|
||||
]
|
||||
|
||||
def initialize(client, concurrency, cloud_auth)
|
||||
def initialize(client, concurrency, cloud_auth, retries)
|
||||
@cloud_auth = cloud_auth
|
||||
@am = ActionManager.new(concurrency, true)
|
||||
@srv_pool = ServicePool.new(@cloud_auth, nil)
|
||||
@retries = retries
|
||||
|
||||
em_conf = {
|
||||
:cloud_auth => @cloud_auth,
|
||||
@ -672,6 +673,34 @@ class ServiceLCM
|
||||
|
||||
private
|
||||
|
||||
# Retry on authentication error
|
||||
#
|
||||
# @param client [OpenNebula::Client] Client to perform operation
|
||||
# @param &block Code block to execute
|
||||
def retry_op(client, &block)
|
||||
finished = false
|
||||
retries = 0
|
||||
rc = nil
|
||||
|
||||
until finished
|
||||
rc = block.call(client)
|
||||
|
||||
if OpenNebula.is_error?(rc) && rc.errno != 256
|
||||
# There is an error different from authentication
|
||||
finished = true
|
||||
elsif !OpenNebula.is_error?(rc) || retries > @retries
|
||||
# There is no error or the retries limit has been reached
|
||||
finished = true
|
||||
else
|
||||
# Error is 256, reset the client to renew the token
|
||||
client = nil
|
||||
retries += 1
|
||||
end
|
||||
end
|
||||
|
||||
rc
|
||||
end
|
||||
|
||||
############################################################################
|
||||
# Callbacks
|
||||
############################################################################
|
||||
@ -679,38 +708,40 @@ class ServiceLCM
|
||||
def deploy_cb(client, service_id, role_name, nodes)
|
||||
undeploy = false
|
||||
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
service.roles[role_name].set_state(Role::STATE['RUNNING'])
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
service.roles[role_name].set_state(Role::STATE['RUNNING'])
|
||||
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
if nodes[node] && service.roles[role_name].cardinalitty > 0
|
||||
service.roles[role_name].cardinality -= 1
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
if nodes[node] && service.roles[role_name].cardinalitty > 0
|
||||
service.roles[role_name].cardinality -= 1
|
||||
end
|
||||
|
||||
nodes[node]
|
||||
end
|
||||
|
||||
nodes[node]
|
||||
# If the role has 0 nodes, deleteƒ role
|
||||
undeploy = service.check_role(service.roles[role_name])
|
||||
|
||||
if service.all_roles_running?
|
||||
service.set_state(Service::STATE['RUNNING'])
|
||||
elsif service.strategy == 'straight'
|
||||
set_deploy_strategy(service)
|
||||
|
||||
deploy_roles(c,
|
||||
service.roles_deploy,
|
||||
'DEPLOYING',
|
||||
'FAILED_DEPLOYING',
|
||||
:wait_deploy_action,
|
||||
service.report_ready?)
|
||||
end
|
||||
|
||||
rc = service.update
|
||||
|
||||
return rc if OpenNebula.is_error?(rc)
|
||||
|
||||
@wd.add_service(service) if service.all_roles_running?
|
||||
end
|
||||
|
||||
# If the role has 0 nodes, deleteƒ role
|
||||
undeploy = service.check_role(service.roles[role_name])
|
||||
|
||||
if service.all_roles_running?
|
||||
service.set_state(Service::STATE['RUNNING'])
|
||||
elsif service.strategy == 'straight'
|
||||
set_deploy_strategy(service)
|
||||
|
||||
deploy_roles(client,
|
||||
service.roles_deploy,
|
||||
'DEPLOYING',
|
||||
'FAILED_DEPLOYING',
|
||||
:wait_deploy_action,
|
||||
service.report_ready?)
|
||||
end
|
||||
|
||||
rc = service.update
|
||||
|
||||
return rc if OpenNebula.is_error?(rc)
|
||||
|
||||
@wd.add_service(service) if service.all_roles_running?
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
@ -723,14 +754,18 @@ class ServiceLCM
|
||||
end
|
||||
|
||||
def deploy_failure_cb(client, service_id, role_name)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
|
||||
service.set_state(Service::STATE['FAILED_DEPLOYING'])
|
||||
service.roles[role_name].set_state(Role::STATE['FAILED_DEPLOYING'])
|
||||
service.set_state(Service::STATE['FAILED_DEPLOYING'])
|
||||
service.roles[role_name].set_state(
|
||||
Role::STATE['FAILED_DEPLOYING']
|
||||
)
|
||||
|
||||
service.update
|
||||
service.update
|
||||
end
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
@ -741,12 +776,14 @@ class ServiceLCM
|
||||
end
|
||||
|
||||
def deploy_nets_failure_cb(client, service_id)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
|
||||
service.set_state(Service::STATE['FAILED_DEPLOYING_NETS'])
|
||||
service.update
|
||||
service.set_state(Service::STATE['FAILED_DEPLOYING_NETS'])
|
||||
service.update
|
||||
end
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
@ -755,36 +792,38 @@ class ServiceLCM
|
||||
def undeploy_cb(client, service_id, role_name, nodes)
|
||||
undeploy_nets = false
|
||||
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
service.roles[role_name].set_state(Role::STATE['DONE'])
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
service.roles[role_name].set_state(Role::STATE['DONE'])
|
||||
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
!nodes[:failure].include?(node['deploy_id']) &&
|
||||
nodes[:successful].include?(node['deploy_id'])
|
||||
end
|
||||
|
||||
if service.all_roles_done?
|
||||
rc = service.delete_networks
|
||||
|
||||
if rc && !rc.empty?
|
||||
Log.info LOG_COMP, 'Error trying to delete '\
|
||||
"Virtual Networks #{rc}"
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
!nodes[:failure].include?(node['deploy_id']) &&
|
||||
nodes[:successful].include?(node['deploy_id'])
|
||||
end
|
||||
|
||||
undeploy_nets = true
|
||||
if service.all_roles_done?
|
||||
rc = service.delete_networks
|
||||
|
||||
break
|
||||
elsif service.strategy == 'straight'
|
||||
set_deploy_strategy(service)
|
||||
if rc && !rc.empty?
|
||||
Log.info LOG_COMP, 'Error trying to delete '\
|
||||
"Virtual Networks #{rc}"
|
||||
end
|
||||
|
||||
undeploy_roles(client,
|
||||
service.roles_shutdown,
|
||||
'UNDEPLOYING',
|
||||
'FAILED_UNDEPLOYING',
|
||||
:wait_undeploy_action)
|
||||
undeploy_nets = true
|
||||
|
||||
break
|
||||
elsif service.strategy == 'straight'
|
||||
set_deploy_strategy(service)
|
||||
|
||||
undeploy_roles(c,
|
||||
service.roles_shutdown,
|
||||
'UNDEPLOYING',
|
||||
'FAILED_UNDEPLOYING',
|
||||
:wait_undeploy_action)
|
||||
end
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
undeploy_nets_action(client, service_id) if undeploy_nets
|
||||
@ -793,128 +832,149 @@ class ServiceLCM
|
||||
end
|
||||
|
||||
def undeploy_nets_cb(client, service_id)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
service.delete
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
service.delete
|
||||
end
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
def undeploy_nets_failure_cb(client, service_id)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
|
||||
service.set_state(Service::STATE['FAILED_UNDEPLOYING_NETS'])
|
||||
service.update
|
||||
service.set_state(Service::STATE['FAILED_UNDEPLOYING_NETS'])
|
||||
service.update
|
||||
end
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
def undeploy_failure_cb(client, service_id, role_name, nodes)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
|
||||
service.set_state(Service::STATE['FAILED_UNDEPLOYING'])
|
||||
service.roles[role_name]
|
||||
.set_state(Role::STATE['FAILED_UNDEPLOYING'])
|
||||
service.set_state(Service::STATE['FAILED_UNDEPLOYING'])
|
||||
service.roles[role_name].set_state(
|
||||
Role::STATE['FAILED_UNDEPLOYING']
|
||||
)
|
||||
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
!nodes[:failure].include?(node['deploy_id']) &&
|
||||
nodes[:successful].include?(node['deploy_id'])
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
!nodes[:failure].include?(node['deploy_id']) &&
|
||||
nodes[:successful].include?(node['deploy_id'])
|
||||
end
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
def scaleup_cb(client, service_id, role_name, nodes)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
if nodes[node] && service.roles[role_name].cardinalitty > 0
|
||||
service.roles[role_name].cardinality -= 1
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
if nodes[node] && service.roles[role_name].cardinalitty > 0
|
||||
service.roles[role_name].cardinality -= 1
|
||||
end
|
||||
|
||||
nodes[node]
|
||||
end
|
||||
|
||||
nodes[node]
|
||||
service.set_state(Service::STATE['COOLDOWN'])
|
||||
service.roles[role_name].set_state(Role::STATE['COOLDOWN'])
|
||||
|
||||
@event_manager.trigger_action(
|
||||
:wait_cooldown_action,
|
||||
service.id,
|
||||
c,
|
||||
service.id,
|
||||
role_name,
|
||||
service.roles[role_name].cooldown
|
||||
)
|
||||
|
||||
service.roles[role_name].clean_scale_way
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
service.set_state(Service::STATE['COOLDOWN'])
|
||||
service.roles[role_name].set_state(Role::STATE['COOLDOWN'])
|
||||
|
||||
@event_manager.trigger_action(:wait_cooldown_action,
|
||||
service.id,
|
||||
client,
|
||||
service.id,
|
||||
role_name,
|
||||
service.roles[role_name].cooldown)
|
||||
|
||||
service.roles[role_name].clean_scale_way
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
def scaledown_cb(client, service_id, role_name, nodes)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
service.set_state(Service::STATE['COOLDOWN'])
|
||||
service.roles[role_name].set_state(Role::STATE['COOLDOWN'])
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
service.set_state(Service::STATE['COOLDOWN'])
|
||||
service.roles[role_name].set_state(Role::STATE['COOLDOWN'])
|
||||
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
!nodes[:failure].include?(node['deploy_id']) &&
|
||||
nodes[:successful].include?(node['deploy_id'])
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
!nodes[:failure].include?(node['deploy_id']) &&
|
||||
nodes[:successful].include?(node['deploy_id'])
|
||||
end
|
||||
|
||||
@event_manager.trigger_action(
|
||||
:wait_cooldown_action,
|
||||
service.id,
|
||||
c,
|
||||
service.id,
|
||||
role_name,
|
||||
service.roles[role_name].cooldown
|
||||
)
|
||||
|
||||
service.roles[role_name].clean_scale_way
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
@event_manager.trigger_action(:wait_cooldown_action,
|
||||
service.id,
|
||||
client,
|
||||
service.id,
|
||||
role_name,
|
||||
service.roles[role_name].cooldown)
|
||||
|
||||
service.roles[role_name].clean_scale_way
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
def scaleup_failure_cb(client, service_id, role_name)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
|
||||
service.set_state(Service::STATE['FAILED_SCALING'])
|
||||
service.roles[role_name].set_state(Role::STATE['FAILED_SCALING'])
|
||||
service.set_state(Service::STATE['FAILED_SCALING'])
|
||||
service.roles[role_name].set_state(
|
||||
Role::STATE['FAILED_SCALING']
|
||||
)
|
||||
|
||||
service.update
|
||||
service.update
|
||||
end
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
def scaledown_failure_cb(client, service_id, role_name, nodes)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
|
||||
role = service.roles[role_name]
|
||||
role = service.roles[role_name]
|
||||
|
||||
service.set_state(Service::STATE['FAILED_SCALING'])
|
||||
role.set_state(Role::STATE['FAILED_SCALING'])
|
||||
service.set_state(Service::STATE['FAILED_SCALING'])
|
||||
role.set_state(Role::STATE['FAILED_SCALING'])
|
||||
|
||||
role.nodes.delete_if do |node|
|
||||
!nodes[:failure].include?(node['deploy_id']) &&
|
||||
nodes[:successful].include?(node['deploy_id'])
|
||||
role.nodes.delete_if do |node|
|
||||
!nodes[:failure].include?(node['deploy_id']) &&
|
||||
nodes[:successful].include?(node['deploy_id'])
|
||||
end
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
@ -923,16 +983,18 @@ class ServiceLCM
|
||||
def cooldown_cb(client, service_id, role_name)
|
||||
undeploy = false
|
||||
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
service.set_state(Service::STATE['RUNNING'])
|
||||
service.roles[role_name].set_state(Role::STATE['RUNNING'])
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
service.set_state(Service::STATE['RUNNING'])
|
||||
service.roles[role_name].set_state(Role::STATE['RUNNING'])
|
||||
|
||||
service.update
|
||||
service.update
|
||||
|
||||
# If the role has 0 nodes, delete role
|
||||
undeploy = service.check_role(service.roles[role_name])
|
||||
# If the role has 0 nodes, delete role
|
||||
undeploy = service.check_role(service.roles[role_name])
|
||||
|
||||
@wd.add_service(service)
|
||||
@wd.add_service(service)
|
||||
end
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
@ -945,49 +1007,10 @@ class ServiceLCM
|
||||
end
|
||||
|
||||
def add_cb(client, service_id, role_name, _)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
service.roles[role_name].set_state(Role::STATE['RUNNING'])
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
service.roles[role_name].set_state(Role::STATE['RUNNING'])
|
||||
|
||||
service.set_state(Service::STATE['RUNNING'])
|
||||
|
||||
rc = service.update
|
||||
|
||||
return rc if OpenNebula.is_error?(rc)
|
||||
|
||||
@wd.add_service(service) if service.all_roles_running?
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
def add_failure_cb(client, service_id, role_name)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
|
||||
service.set_state(Service::STATE['FAILED_DEPLOYING'])
|
||||
service.roles[role_name].set_state(Role::STATE['FAILED_DEPLOYING'])
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
def remove_cb(client, service_id, role_name, _)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
service.remove_role(role_name)
|
||||
|
||||
if service.all_roles_done?
|
||||
rc = service.delete_networks
|
||||
|
||||
if rc && !rc.empty?
|
||||
Log.info LOG_COMP, 'Error trying to delete '\
|
||||
"Virtual Networks #{rc}"
|
||||
end
|
||||
|
||||
service.delete
|
||||
else
|
||||
service.set_state(Service::STATE['RUNNING'])
|
||||
|
||||
rc = service.update
|
||||
@ -1001,49 +1024,103 @@ class ServiceLCM
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
def remove_failure_cb(client, service_id, role_name, nodes)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
def add_failure_cb(client, service_id, role_name)
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
|
||||
service.set_state(Service::STATE['FAILED_UNDEPLOYING'])
|
||||
service.roles[role_name]
|
||||
.set_state(Role::STATE['FAILED_UNDEPLOYING'])
|
||||
service.set_state(Service::STATE['FAILED_DEPLOYING'])
|
||||
service.roles[role_name].set_state(
|
||||
Role::STATE['FAILED_DEPLOYING']
|
||||
)
|
||||
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
!nodes[:failure].include?(node['deploy_id']) &&
|
||||
nodes[:successful].include?(node['deploy_id'])
|
||||
service.update
|
||||
end
|
||||
end
|
||||
|
||||
service.update
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
def remove_cb(client, service_id, role_name, _)
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
service.remove_role(role_name)
|
||||
|
||||
if service.all_roles_done?
|
||||
rc = service.delete_networks
|
||||
|
||||
if rc && !rc.empty?
|
||||
Log.info LOG_COMP, 'Error trying to delete '\
|
||||
"Virtual Networks #{rc}"
|
||||
end
|
||||
|
||||
service.delete
|
||||
else
|
||||
service.set_state(Service::STATE['RUNNING'])
|
||||
|
||||
rc = service.update
|
||||
|
||||
return rc if OpenNebula.is_error?(rc)
|
||||
|
||||
@wd.add_service(service) if service.all_roles_running?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
def remove_failure_cb(client, service_id, role_name, nodes)
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
# stop actions for the service if deploy fails
|
||||
@event_manager.cancel_action(service_id)
|
||||
|
||||
service.set_state(Service::STATE['FAILED_UNDEPLOYING'])
|
||||
service.roles[role_name].set_state(
|
||||
Role::STATE['FAILED_UNDEPLOYING']
|
||||
)
|
||||
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
!nodes[:failure].include?(node['deploy_id']) &&
|
||||
nodes[:successful].include?(node['deploy_id'])
|
||||
end
|
||||
|
||||
service.update
|
||||
end
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
def hold_cb(client, service_id, role_name)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
if service.roles[role_name].state != Role::STATE['HOLD']
|
||||
service.roles[role_name].set_state(Role::STATE['HOLD'])
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
if service.roles[role_name].state != Role::STATE['HOLD']
|
||||
service.roles[role_name].set_state(Role::STATE['HOLD'])
|
||||
end
|
||||
|
||||
if service.all_roles_hold? &&
|
||||
service.state != Service::STATE['HOLD']
|
||||
service.set_state(Service::STATE['HOLD'])
|
||||
elsif service.strategy == 'straight'
|
||||
set_deploy_strategy(service)
|
||||
|
||||
deploy_roles(
|
||||
c,
|
||||
service.roles_hold,
|
||||
'DEPLOYING',
|
||||
'FAILED_DEPLOYING',
|
||||
:wait_deploy_action,
|
||||
service.report_ready?
|
||||
)
|
||||
end
|
||||
|
||||
rc = service.update
|
||||
|
||||
return rc if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
if service.all_roles_hold? &&
|
||||
service.state != Service::STATE['HOLD']
|
||||
service.set_state(Service::STATE['HOLD'])
|
||||
elsif service.strategy == 'straight'
|
||||
set_deploy_strategy(service)
|
||||
|
||||
deploy_roles(client,
|
||||
service.roles_hold,
|
||||
'DEPLOYING',
|
||||
'FAILED_DEPLOYING',
|
||||
:wait_deploy_action,
|
||||
service.report_ready?)
|
||||
end
|
||||
|
||||
rc = service.update
|
||||
|
||||
return rc if OpenNebula.is_error?(rc)
|
||||
end
|
||||
|
||||
Log.error 'WD', rc.message if OpenNebula.is_error?(rc)
|
||||
@ -1052,38 +1129,42 @@ class ServiceLCM
|
||||
def release_cb(client, service_id, role_name, nodes)
|
||||
undeploy = false
|
||||
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
service.roles[role_name].set_state(Role::STATE['RUNNING'])
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
service.roles[role_name].set_state(Role::STATE['RUNNING'])
|
||||
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
if nodes[node] && service.roles[role_name].cardinalitty > 0
|
||||
service.roles[role_name].cardinality -= 1
|
||||
service.roles[role_name].nodes.delete_if do |node|
|
||||
if nodes[node] && service.roles[role_name].cardinalitty > 0
|
||||
service.roles[role_name].cardinality -= 1
|
||||
end
|
||||
|
||||
nodes[node]
|
||||
end
|
||||
|
||||
nodes[node]
|
||||
# If the role has 0 nodes, delete role
|
||||
undeploy = service.check_role(service.roles[role_name])
|
||||
|
||||
if service.all_roles_running?
|
||||
service.set_state(Service::STATE['RUNNING'])
|
||||
elsif service.strategy == 'straight'
|
||||
set_deploy_strategy(service)
|
||||
|
||||
release_roles(
|
||||
c,
|
||||
service.roles_release,
|
||||
'DEPLOYING',
|
||||
'FAILED_DEPLOYING',
|
||||
:wait_deploy_action,
|
||||
service.report_ready?
|
||||
)
|
||||
end
|
||||
|
||||
rc = service.update
|
||||
|
||||
return rc if OpenNebula.is_error?(rc)
|
||||
|
||||
@wd.add_service(service) if service.all_roles_running?
|
||||
end
|
||||
|
||||
# If the role has 0 nodes, delete role
|
||||
undeploy = service.check_role(service.roles[role_name])
|
||||
|
||||
if service.all_roles_running?
|
||||
service.set_state(Service::STATE['RUNNING'])
|
||||
elsif service.strategy == 'straight'
|
||||
set_deploy_strategy(service)
|
||||
|
||||
release_roles(client,
|
||||
service.roles_release,
|
||||
'DEPLOYING',
|
||||
'FAILED_DEPLOYING',
|
||||
:wait_deploy_action,
|
||||
service.report_ready?)
|
||||
end
|
||||
|
||||
rc = service.update
|
||||
|
||||
return rc if OpenNebula.is_error?(rc)
|
||||
|
||||
@wd.add_service(service) if service.all_roles_running?
|
||||
end
|
||||
|
||||
Log.error LOG_COMP, rc.message if OpenNebula.is_error?(rc)
|
||||
@ -1100,16 +1181,18 @@ class ServiceLCM
|
||||
############################################################################
|
||||
|
||||
def error_wd_cb(client, service_id, role_name, _node)
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
if service.state != Service::STATE['WARNING']
|
||||
service.set_state(Service::STATE['WARNING'])
|
||||
end
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
if service.state != Service::STATE['WARNING']
|
||||
service.set_state(Service::STATE['WARNING'])
|
||||
end
|
||||
|
||||
if service.roles[role_name].state != Role::STATE['WARNING']
|
||||
service.roles[role_name].set_state(Role::STATE['WARNING'])
|
||||
end
|
||||
if service.roles[role_name].state != Role::STATE['WARNING']
|
||||
service.roles[role_name].set_state(Role::STATE['WARNING'])
|
||||
end
|
||||
|
||||
service.update
|
||||
service.update
|
||||
end
|
||||
end
|
||||
|
||||
Log.error 'WD', rc.message if OpenNebula.is_error?(rc)
|
||||
@ -1118,29 +1201,31 @@ class ServiceLCM
|
||||
def done_wd_cb(client, service_id, role_name, node)
|
||||
undeploy = false
|
||||
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
role = service.roles[role_name]
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
role = service.roles[role_name]
|
||||
|
||||
next unless role
|
||||
next unless role
|
||||
|
||||
cardinality = role.cardinality - 1
|
||||
cardinality = role.cardinality - 1
|
||||
|
||||
next unless role.nodes.find {|n| n['deploy_id'] == node }
|
||||
next unless role.nodes.find {|n| n['deploy_id'] == node }
|
||||
|
||||
# just update if the cardinality is positive
|
||||
set_cardinality(role, cardinality, true) if cardinality >= 0
|
||||
# just update if the cardinality is positive
|
||||
set_cardinality(role, cardinality, true) if cardinality >= 0
|
||||
|
||||
role.nodes.delete_if {|n| n['deploy_id'] == node }
|
||||
role.nodes.delete_if {|n| n['deploy_id'] == node }
|
||||
|
||||
# If the role has 0 nodes, delete role
|
||||
undeploy = service.check_role(role)
|
||||
# If the role has 0 nodes, delete role
|
||||
undeploy = service.check_role(role)
|
||||
|
||||
service.update
|
||||
service.update
|
||||
|
||||
Log.info 'WD',
|
||||
"Node #{node} is done, " \
|
||||
"updating service #{service_id}:#{role_name} " \
|
||||
"cardinality to #{cardinality}"
|
||||
Log.info 'WD',
|
||||
"Node #{node} is done, " \
|
||||
"updating service #{service_id}:#{role_name} " \
|
||||
"cardinality to #{cardinality}"
|
||||
end
|
||||
end
|
||||
|
||||
Log.error 'WD', rc.message if OpenNebula.is_error?(rc)
|
||||
@ -1155,22 +1240,24 @@ class ServiceLCM
|
||||
def running_wd_cb(client, service_id, role_name, _node)
|
||||
undeploy = false
|
||||
|
||||
rc = @srv_pool.get(service_id, client) do |service|
|
||||
role = service.roles[role_name]
|
||||
rc = retry_op(client) do |c|
|
||||
@srv_pool.get(service_id, c) do |service|
|
||||
role = service.roles[role_name]
|
||||
|
||||
if service.roles[role_name].state != Role::STATE['RUNNING']
|
||||
service.roles[role_name].set_state(Role::STATE['RUNNING'])
|
||||
if service.roles[role_name].state != Role::STATE['RUNNING']
|
||||
service.roles[role_name].set_state(Role::STATE['RUNNING'])
|
||||
end
|
||||
|
||||
if service.all_roles_running? &&
|
||||
service.state != Service::STATE['RUNNING']
|
||||
service.set_state(Service::STATE['RUNNING'])
|
||||
end
|
||||
|
||||
# If the role has 0 nodes, delete role
|
||||
undeploy = service.check_role(role)
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
if service.all_roles_running? &&
|
||||
service.state != Service::STATE['RUNNING']
|
||||
service.set_state(Service::STATE['RUNNING'])
|
||||
end
|
||||
|
||||
# If the role has 0 nodes, delete role
|
||||
undeploy = service.check_role(role)
|
||||
|
||||
service.update
|
||||
end
|
||||
|
||||
Log.error 'WD', rc.message if OpenNebula.is_error?(rc)
|
||||
|
@ -102,8 +102,9 @@ conf[:action_period] ||= 60
|
||||
conf[:vm_name_template] ||= DEFAULT_VM_NAME_TEMPLATE
|
||||
conf[:wait_timeout] ||= 30
|
||||
conf[:concurrency] ||= 10
|
||||
conf[:auth] = 'opennebula'
|
||||
conf[:auth] = 'opennebula'
|
||||
conf[:page_size] ||= 10
|
||||
conf[:retries] ||= 5
|
||||
|
||||
set :bind, conf[:host]
|
||||
set :port, conf[:port]
|
||||
@ -210,7 +211,7 @@ GENERAL_EC = 500 # general error
|
||||
##############################################################################
|
||||
|
||||
# TODO: make thread number configurable?
|
||||
lcm = ServiceLCM.new(@client, conf[:concurrency], cloud_auth)
|
||||
lcm = ServiceLCM.new(@client, conf[:concurrency], cloud_auth, conf[:retries])
|
||||
|
||||
##############################################################################
|
||||
# Service
|
||||
|
@ -147,6 +147,14 @@ devices.each do |dev|
|
||||
# name, in this way Sunstone shows a better name
|
||||
values << pval('DEVICE_NAME',
|
||||
"#{dev[:vendor_name]} #{dev[:device_name]}")
|
||||
|
||||
# Get profiles
|
||||
addr = "0000:#{dev[:bus]}:#{dev[:slot]}.#{dev[:function]}"
|
||||
profiles = `ls /sys/class/mdev_bus/#{addr}/mdev_supported_types`
|
||||
profiles = profiles.split("\n")
|
||||
|
||||
# Comma separated value with different profiles
|
||||
values << pval('PROFILES', profiles.join(','))
|
||||
else
|
||||
values << pval('DEVICE_NAME', dev[:device_name])
|
||||
end
|
||||
|
@ -1228,13 +1228,6 @@ int ImageManager::revert_snapshot(int iid, int sid, string& error)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (snaps.get_active_id() == sid)
|
||||
{
|
||||
error = "Snapshot is already the active one";
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Format message and send action to driver */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
@ -67,6 +67,7 @@ void ImageManager::_cp(unique_ptr<image_msg_t> msg)
|
||||
|
||||
ostringstream oss;
|
||||
istringstream is(info);
|
||||
is >> skipws;
|
||||
|
||||
auto image = ipool->get(msg->oid());
|
||||
|
||||
@ -74,7 +75,7 @@ void ImageManager::_cp(unique_ptr<image_msg_t> msg)
|
||||
{
|
||||
if (msg->status() == "SUCCESS")
|
||||
{
|
||||
is >> source >> ws;
|
||||
is >> source;
|
||||
|
||||
if (!source.empty())
|
||||
{
|
||||
@ -95,14 +96,14 @@ void ImageManager::_cp(unique_ptr<image_msg_t> msg)
|
||||
goto error;
|
||||
}
|
||||
|
||||
is >> source >> ws;
|
||||
is >> source;
|
||||
|
||||
if (is.fail())
|
||||
{
|
||||
goto error;
|
||||
}
|
||||
|
||||
is >> format >> ws;
|
||||
is >> format;
|
||||
|
||||
if (is.fail() || format.empty())
|
||||
{
|
||||
|
@ -225,6 +225,7 @@ void LifeCycleManager::trigger_migrate(int vid, const RequestAttributes& ra,
|
||||
|
||||
trigger([this, vid, uid, gid, req_id, vm_action] {
|
||||
HostShareCapacity sr;
|
||||
Template quota_tmpl;
|
||||
|
||||
time_t the_time = time(0);
|
||||
|
||||
@ -313,7 +314,7 @@ void LifeCycleManager::trigger_migrate(int vid, const RequestAttributes& ra,
|
||||
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots();
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
}
|
||||
|
||||
vm->set_action(VMActions::MIGRATE_ACTION, uid, gid, req_id);
|
||||
@ -345,6 +346,13 @@ void LifeCycleManager::trigger_migrate(int vid, const RequestAttributes& ra,
|
||||
{
|
||||
vm->log("LCM", Log::ERROR, "migrate_action, VM in a wrong state.");
|
||||
}
|
||||
|
||||
vm.reset();
|
||||
|
||||
if (!quota_tmpl.empty())
|
||||
{
|
||||
Quotas::quota_del(Quotas::VM, uid, gid, "a_tmpl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -846,6 +854,7 @@ void LifeCycleManager::trigger_delete(int vid, const RequestAttributes& ra)
|
||||
|
||||
trigger([this, vid, uid, gid, req_id] {
|
||||
int image_id = -1;
|
||||
Template quota_tmpl;
|
||||
|
||||
if ( auto vm = vmpool->get(vid) )
|
||||
{
|
||||
@ -867,7 +876,7 @@ void LifeCycleManager::trigger_delete(int vid, const RequestAttributes& ra)
|
||||
break;
|
||||
|
||||
default:
|
||||
clean_up_vm(vm.get(), true, image_id, uid, gid, req_id);
|
||||
clean_up_vm(vm.get(), true, image_id, uid, gid, req_id, quota_tmpl);
|
||||
dm->trigger_done(vid);
|
||||
break;
|
||||
}
|
||||
@ -886,6 +895,11 @@ void LifeCycleManager::trigger_delete(int vid, const RequestAttributes& ra)
|
||||
ipool->update(image.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (!quota_tmpl.empty())
|
||||
{
|
||||
Quotas::quota_del(Quotas::VM, uid, gid, "a_tmpl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -900,7 +914,7 @@ void LifeCycleManager::trigger_delete_recreate(int vid,
|
||||
int req_id = ra.req_id;
|
||||
|
||||
trigger([this, vid, uid, gid, req_id] {
|
||||
Template * vm_quotas_snp = nullptr;
|
||||
Template vm_quotas_snp;
|
||||
|
||||
vector<Template *> ds_quotas_snp;
|
||||
|
||||
@ -931,11 +945,13 @@ void LifeCycleManager::trigger_delete_recreate(int vid,
|
||||
vm_uid = vm->get_uid();
|
||||
vm_gid = vm->get_gid();
|
||||
|
||||
clean_up_vm(vm.get(), false, image_id, uid, gid, req_id);
|
||||
clean_up_vm(vm.get(), false, image_id, uid, gid, req_id, vm_quotas_snp);
|
||||
|
||||
vm->delete_non_persistent_disk_snapshots(&vm_quotas_snp,
|
||||
vm->delete_non_persistent_disk_snapshots(vm_quotas_snp,
|
||||
ds_quotas_snp);
|
||||
|
||||
vm->delete_snapshots(vm_quotas_snp);
|
||||
|
||||
vmpool->update(vm.get());
|
||||
break;
|
||||
}
|
||||
@ -960,11 +976,9 @@ void LifeCycleManager::trigger_delete_recreate(int vid,
|
||||
Quotas::ds_del_recreate(vm_uid, vm_gid, ds_quotas_snp);
|
||||
}
|
||||
|
||||
if ( vm_quotas_snp != nullptr )
|
||||
if ( !vm_quotas_snp.empty())
|
||||
{
|
||||
Quotas::vm_del(vm_uid, vm_gid, vm_quotas_snp);
|
||||
|
||||
delete vm_quotas_snp;
|
||||
Quotas::vm_del(vm_uid, vm_gid, &vm_quotas_snp);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -973,7 +987,7 @@ void LifeCycleManager::trigger_delete_recreate(int vid,
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
void LifeCycleManager::clean_up_vm(VirtualMachine * vm, bool dispose,
|
||||
int& image_id, int uid, int gid, int req_id)
|
||||
int& image_id, int uid, int gid, int req_id, Template& quota_tmpl)
|
||||
{
|
||||
HostShareCapacity sr;
|
||||
|
||||
@ -999,7 +1013,7 @@ void LifeCycleManager::clean_up_vm(VirtualMachine * vm, bool dispose,
|
||||
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots();
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
}
|
||||
|
||||
if (vm->get_etime() == 0)
|
||||
|
@ -40,11 +40,6 @@ void LifeCycleManager::start_prolog_migrate(VirtualMachine* vm)
|
||||
|
||||
vm->set_state(VirtualMachine::PROLOG_MIGRATE);
|
||||
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots();
|
||||
}
|
||||
|
||||
vm->set_previous_etime(the_time);
|
||||
|
||||
vm->set_previous_running_etime(the_time);
|
||||
@ -55,8 +50,6 @@ void LifeCycleManager::start_prolog_migrate(VirtualMachine* vm)
|
||||
|
||||
vmpool->update_history(vm);
|
||||
|
||||
vmpool->update(vm);
|
||||
|
||||
vm->get_capacity(sr);
|
||||
|
||||
if ( vm->get_hid() != vm->get_previous_hid() )
|
||||
@ -147,8 +140,17 @@ void LifeCycleManager::trigger_save_success(int vid)
|
||||
return;
|
||||
}
|
||||
|
||||
Template quota_tmpl;
|
||||
int uid = vm->get_uid();
|
||||
int gid = vm->get_gid();
|
||||
|
||||
if ( vm->get_lcm_state() == VirtualMachine::SAVE_MIGRATE )
|
||||
{
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
}
|
||||
|
||||
start_prolog_migrate(vm.get());
|
||||
}
|
||||
else if (vm->get_lcm_state() == VirtualMachine::SAVE_SUSPEND)
|
||||
@ -159,7 +161,7 @@ void LifeCycleManager::trigger_save_success(int vid)
|
||||
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots();
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
|
||||
vmpool->update(vm.get());
|
||||
}
|
||||
@ -180,7 +182,7 @@ void LifeCycleManager::trigger_save_success(int vid)
|
||||
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots();
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
}
|
||||
|
||||
vm->set_epilog_stime(the_time);
|
||||
@ -199,6 +201,13 @@ void LifeCycleManager::trigger_save_success(int vid)
|
||||
{
|
||||
vm->log("LCM",Log::ERROR,"save_success_action, VM in a wrong state");
|
||||
}
|
||||
|
||||
vm.reset();
|
||||
|
||||
if (!quota_tmpl.empty())
|
||||
{
|
||||
Quotas::quota_del(Quotas::VM, uid, gid, "a_tmpl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -257,6 +266,10 @@ void LifeCycleManager::trigger_deploy_success(int vid)
|
||||
return;
|
||||
}
|
||||
|
||||
Template quota_tmpl;
|
||||
int uid = vm->get_uid();
|
||||
int gid = vm->get_gid();
|
||||
|
||||
//----------------------------------------------------
|
||||
// RUNNING STATE
|
||||
//----------------------------------------------------
|
||||
@ -285,7 +298,7 @@ void LifeCycleManager::trigger_deploy_success(int vid)
|
||||
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots();
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
}
|
||||
|
||||
vm->release_previous_vnc_port();
|
||||
@ -320,6 +333,13 @@ void LifeCycleManager::trigger_deploy_success(int vid)
|
||||
{
|
||||
vm->log("LCM",Log::ERROR,"deploy_success_action, VM in a wrong state");
|
||||
}
|
||||
|
||||
vm.reset();
|
||||
|
||||
if (!quota_tmpl.empty())
|
||||
{
|
||||
Quotas::quota_del(Quotas::VM, uid, gid, "a_tmpl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -447,6 +467,10 @@ void LifeCycleManager::trigger_shutdown_success(int vid)
|
||||
return;
|
||||
}
|
||||
|
||||
Template quota_tmpl;
|
||||
int uid = vm->get_uid();
|
||||
int gid = vm->get_gid();
|
||||
|
||||
if ( vm->get_lcm_state() == VirtualMachine::SHUTDOWN )
|
||||
{
|
||||
//----------------------------------------------------
|
||||
@ -456,7 +480,7 @@ void LifeCycleManager::trigger_shutdown_success(int vid)
|
||||
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots();
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
}
|
||||
|
||||
vm->set_epilog_stime(the_time);
|
||||
@ -479,7 +503,7 @@ void LifeCycleManager::trigger_shutdown_success(int vid)
|
||||
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots();
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
|
||||
vmpool->update(vm.get());
|
||||
}
|
||||
@ -498,7 +522,7 @@ void LifeCycleManager::trigger_shutdown_success(int vid)
|
||||
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots();
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
}
|
||||
|
||||
vm->set_epilog_stime(the_time);
|
||||
@ -515,12 +539,24 @@ void LifeCycleManager::trigger_shutdown_success(int vid)
|
||||
}
|
||||
else if (vm->get_lcm_state() == VirtualMachine::SAVE_MIGRATE)
|
||||
{
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
}
|
||||
|
||||
start_prolog_migrate(vm.get());
|
||||
}
|
||||
else
|
||||
{
|
||||
vm->log("LCM",Log::ERROR,"shutdown_success_action, VM in a wrong state");
|
||||
}
|
||||
|
||||
vm.reset();
|
||||
|
||||
if (!quota_tmpl.empty())
|
||||
{
|
||||
Quotas::quota_del(Quotas::VM, uid, gid, "a_tmpl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -583,6 +619,10 @@ void LifeCycleManager::trigger_prolog_success(int vid)
|
||||
return;
|
||||
}
|
||||
|
||||
Template quota_tmpl;
|
||||
int uid = vm->get_uid();
|
||||
int gid = vm->get_gid();
|
||||
|
||||
VirtualMachine::LcmState lcm_state = vm->get_lcm_state();
|
||||
|
||||
switch (lcm_state)
|
||||
@ -658,7 +698,7 @@ void LifeCycleManager::trigger_prolog_success(int vid)
|
||||
case VirtualMachine::PROLOG_MIGRATE_SUSPEND_FAILURE: //recover success
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots();
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
}
|
||||
|
||||
vm->set_prolog_etime(the_time);
|
||||
@ -682,6 +722,13 @@ void LifeCycleManager::trigger_prolog_success(int vid)
|
||||
vm->log("LCM",Log::ERROR,"prolog_success_action, VM in a wrong state");
|
||||
break;
|
||||
}
|
||||
|
||||
vm.reset();
|
||||
|
||||
if (!quota_tmpl.empty())
|
||||
{
|
||||
Quotas::quota_del(Quotas::VM, uid, gid, "a_tmpl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -982,6 +1029,10 @@ void LifeCycleManager::trigger_monitor_suspend(int vid)
|
||||
return;
|
||||
}
|
||||
|
||||
Template quota_tmpl;
|
||||
int uid = vm->get_uid();
|
||||
int gid = vm->get_gid();
|
||||
|
||||
if ( vm->get_lcm_state() == VirtualMachine::RUNNING ||
|
||||
vm->get_lcm_state() == VirtualMachine::UNKNOWN )
|
||||
{
|
||||
@ -997,7 +1048,7 @@ void LifeCycleManager::trigger_monitor_suspend(int vid)
|
||||
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots();
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
}
|
||||
|
||||
vm->set_internal_action(VMActions::MONITOR_ACTION);
|
||||
@ -1014,6 +1065,13 @@ void LifeCycleManager::trigger_monitor_suspend(int vid)
|
||||
{
|
||||
vm->log("LCM",Log::ERROR,"monitor_suspend_action, VM in a wrong state");
|
||||
}
|
||||
|
||||
vm.reset();
|
||||
|
||||
if (!quota_tmpl.empty())
|
||||
{
|
||||
Quotas::quota_del(Quotas::VM, uid, gid, "a_tmpl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1061,6 +1119,10 @@ void LifeCycleManager::trigger_monitor_poweroff(int vid)
|
||||
return;
|
||||
}
|
||||
|
||||
Template quota_tmpl;
|
||||
int uid = vm->get_uid();
|
||||
int gid = vm->get_gid();
|
||||
|
||||
if ( vm->get_lcm_state() == VirtualMachine::RUNNING ||
|
||||
vm->get_lcm_state() == VirtualMachine::UNKNOWN )
|
||||
{
|
||||
@ -1072,7 +1134,7 @@ void LifeCycleManager::trigger_monitor_poweroff(int vid)
|
||||
|
||||
if ( !vmm->is_keep_snapshots(vm->get_vmm_mad()) )
|
||||
{
|
||||
vm->delete_snapshots();
|
||||
vm->delete_snapshots(quota_tmpl);
|
||||
}
|
||||
|
||||
vm->set_resched(false);
|
||||
@ -1098,6 +1160,13 @@ void LifeCycleManager::trigger_monitor_poweroff(int vid)
|
||||
|
||||
trigger_shutdown_success(vid);
|
||||
}
|
||||
|
||||
vm.reset();
|
||||
|
||||
if (!quota_tmpl.empty())
|
||||
{
|
||||
Quotas::quota_del(Quotas::VM, uid, gid, "a_tmpl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1429,9 +1498,11 @@ void LifeCycleManager::trigger_snapshot_create_failure(int vid)
|
||||
if (snap)
|
||||
{
|
||||
Template quota_tmpl;
|
||||
quota_tmpl.set(snap);
|
||||
|
||||
Quotas::vm_del(vm_uid, vm_gid, "a_tmpl);
|
||||
quota_tmpl.set(snap);
|
||||
quota_tmpl.replace("VMS", 0);
|
||||
|
||||
Quotas::quota_del(Quotas::VM, vm_uid, vm_gid, "a_tmpl);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1516,9 +1587,11 @@ void LifeCycleManager::trigger_snapshot_delete_success(int vid)
|
||||
if (snap)
|
||||
{
|
||||
Template quota_tmpl;
|
||||
quota_tmpl.set(snap);
|
||||
|
||||
Quotas::vm_del(vm_uid, vm_gid, "a_tmpl);
|
||||
quota_tmpl.set(snap);
|
||||
quota_tmpl.replace("VMS", 0);
|
||||
|
||||
Quotas::quota_del(Quotas::VM, vm_uid, vm_gid, "a_tmpl);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -2517,6 +2590,16 @@ void LifeCycleManager::trigger_resize_failure(int vid)
|
||||
deltas.add("CPU", ncpu - ocpu);
|
||||
deltas.add("VMS", 0);
|
||||
|
||||
auto state = vm->get_state();
|
||||
|
||||
if (state == VirtualMachine::PENDING || state == VirtualMachine::HOLD ||
|
||||
(state == VirtualMachine::ACTIVE &&
|
||||
vm->get_lcm_state() == VirtualMachine::RUNNING))
|
||||
{
|
||||
deltas.add("RUNNING_MEMORY", nmem - omem);
|
||||
deltas.add("RUNNING_CPU", ncpu - ocpu);
|
||||
}
|
||||
|
||||
vm->resize(ocpu, omem, ovcpu, error);
|
||||
}
|
||||
else
|
||||
|
@ -112,6 +112,12 @@ class DockerHubMarket
|
||||
regt = Time.new(time[0], time[1], time[2]).to_i
|
||||
end
|
||||
|
||||
if app['description']
|
||||
desc = app['description'].delete('\\"')
|
||||
else
|
||||
desc = ''
|
||||
end
|
||||
|
||||
data = {
|
||||
'NAME' => app['name'],
|
||||
'SOURCE' => app_url(app),
|
||||
@ -125,7 +131,7 @@ class DockerHubMarket
|
||||
'TAGS' => '',
|
||||
'REGTIME' => regt,
|
||||
'SIZE' => @options[:sizemb],
|
||||
'DESCRIPTION' => app['description'].delete('\\"'),
|
||||
'DESCRIPTION' => desc,
|
||||
'LINK' => "https://hub.docker.com/_/#{app['name']}"
|
||||
}
|
||||
|
||||
|
@ -68,6 +68,8 @@ module OpenNebula::TemplateExt
|
||||
|
||||
image = image_lookup(disk)
|
||||
|
||||
next unless image
|
||||
|
||||
if OpenNebula.is_error?(image)
|
||||
logger.fatal image.message if logger
|
||||
|
||||
@ -318,6 +320,9 @@ module OpenNebula::TemplateExt
|
||||
uname = disk['IMAGE_UNAME']
|
||||
uname ||= self['UNAME']
|
||||
|
||||
# Volatile disk
|
||||
return unless name
|
||||
|
||||
name.gsub!('"', '')
|
||||
image = @image_lookup_cache.find do |v|
|
||||
v['NAME'] == name && v['UNAME'] == uname
|
||||
|
@ -40,11 +40,11 @@ module OneDBFsck
|
||||
'but it does not exist')
|
||||
|
||||
doc.root.xpath('CLUSTER_ID').each do |e|
|
||||
e.text = '-1'
|
||||
e.content = '-1'
|
||||
end
|
||||
|
||||
doc.root.xpath('CLUSTER').each do |e|
|
||||
e.text = ''
|
||||
e.content = ''
|
||||
end
|
||||
|
||||
hosts_fix[row[:oid]] = { :body => doc.root.to_s, :cid => -1 }
|
||||
@ -57,7 +57,7 @@ module OneDBFsck
|
||||
"It will be changed to #{new_cluster}")
|
||||
|
||||
doc.root.xpath('CLUSTER').each do |e|
|
||||
e.text = new_cluster
|
||||
e.content = new_cluster
|
||||
end
|
||||
|
||||
hosts_fix[row[:oid]] = { :body => doc.root.to_s,
|
||||
|
@ -309,6 +309,16 @@ module OneDBFsck
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
vmdoc.root.xpath('TEMPLATE/SNAPSHOT').each do |e|
|
||||
size = 0
|
||||
|
||||
size_e = e.at_xpath('SYSTEM_DISK_SIZE')
|
||||
|
||||
size = size_e.text.to_i unless size_e.nil?
|
||||
|
||||
sys_used += size
|
||||
end
|
||||
end
|
||||
|
||||
vm_elem.xpath('SYSTEM_DISK_SIZE_USED').each do |e|
|
||||
|
@ -108,7 +108,12 @@ module OneProvision
|
||||
CEPH_ANSIBLE_DIR.to_s)
|
||||
end
|
||||
|
||||
Driver.run('ansible-galaxy install -r ' <<
|
||||
# with current ansible version we need both commands
|
||||
Driver.run('ansible-galaxy role install -r ' <<
|
||||
'/usr/share/one/oneprovision/ansible/' <<
|
||||
'hci-requirements.yml')
|
||||
|
||||
Driver.run('ansible-galaxy collection install -r ' <<
|
||||
'/usr/share/one/oneprovision/ansible/' <<
|
||||
'hci-requirements.yml')
|
||||
end
|
||||
|
@ -291,6 +291,15 @@ Request::ErrorCode VirtualMachineAllocate::pool_allocate(
|
||||
if ( rc < 0 )
|
||||
{
|
||||
vector<unique_ptr<Template>> ds_quotas;
|
||||
std::string memory, cpu;
|
||||
|
||||
tmpl_back.get("MEMORY", memory);
|
||||
tmpl_back.get("CPU", cpu);
|
||||
|
||||
tmpl_back.add("RUNNING_MEMORY", memory);
|
||||
tmpl_back.add("RUNNING_CPU", cpu);
|
||||
tmpl_back.add("RUNNING_VMS", 1);
|
||||
tmpl_back.add("VMS", 1);
|
||||
|
||||
quota_rollback(&tmpl_back, Quotas::VIRTUALMACHINE, att);
|
||||
|
||||
|
@ -2013,6 +2013,7 @@ void VirtualMachineResize::request_execute(xmlrpc_c::paramList const& paramList,
|
||||
float ncpu, ocpu, dcpu;
|
||||
long nmemory, omemory, dmemory;
|
||||
int nvcpu, ovcpu;
|
||||
bool update_running_quota;
|
||||
|
||||
Template deltas;
|
||||
|
||||
@ -2112,6 +2113,12 @@ void VirtualMachineResize::request_execute(xmlrpc_c::paramList const& paramList,
|
||||
{
|
||||
ncpu = nvcpu;
|
||||
}
|
||||
|
||||
auto state = vm->get_state();
|
||||
|
||||
update_running_quota = state == VirtualMachine::PENDING ||
|
||||
state == VirtualMachine::HOLD || (state == VirtualMachine::ACTIVE &&
|
||||
vm->get_lcm_state() == VirtualMachine::RUNNING);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -2142,6 +2149,12 @@ void VirtualMachineResize::request_execute(xmlrpc_c::paramList const& paramList,
|
||||
deltas.add("CPU", dcpu);
|
||||
deltas.add("VMS", 0);
|
||||
|
||||
if (update_running_quota)
|
||||
{
|
||||
deltas.add("RUNNING_MEMORY", dmemory);
|
||||
deltas.add("RUNNING_CPU", dcpu);
|
||||
}
|
||||
|
||||
if (quota_resize_authorization(&deltas, att, vm_perms) == false)
|
||||
{
|
||||
failure_response(AUTHORIZATION, att);
|
||||
|
@ -320,7 +320,7 @@ class SunstoneServer < CloudServer
|
||||
# Guacamole
|
||||
########################################################################
|
||||
def startguac(id, type_connection, guac, client=nil)
|
||||
resource = retrieve_resource("vm", id)
|
||||
resource = retrieve_resource("vm", id, true)
|
||||
if OpenNebula.is_error?(resource)
|
||||
return [404, resource.to_json]
|
||||
end
|
||||
|
@ -184,7 +184,7 @@ define(function(require) {
|
||||
|
||||
function leasesClock(element){
|
||||
var rtn = "";
|
||||
// The charter info is pulled from the schedule action of the first VM of the first role
|
||||
// The charter info is pulled from the schedule action of the first VM of the first role
|
||||
// (element.TEMPLATE.BODY.roles[0].vm_template_contents.SCHED_ACTION)
|
||||
if(element && element.TEMPLATE && element.TEMPLATE.BODY && element.TEMPLATE.BODY.start_time && element.TEMPLATE.BODY.roles){
|
||||
var startTime = element.TEMPLATE.BODY.start_time;
|
||||
@ -205,8 +205,10 @@ define(function(require) {
|
||||
!isNaN(parseInt(leases[action.ACTION].time)) &&
|
||||
leases[action.ACTION].color
|
||||
){
|
||||
if(checkTime(startTime, action.TIME)){
|
||||
rtn = $("<i/>",{class:"describeCharter fa fa-clock",data_start:startTime, data_add:action.TIME, data_action:action.ACTION}).css({"position":"relative","color":leases[action.ACTION].color});
|
||||
var endTime = (action.TIME.startsWith("+")? action.TIME : action.TIME - startTime).toString();
|
||||
|
||||
if(checkTime(startTime, endTime)){
|
||||
rtn = $("<i/>",{class:"describeCharter fa fa-clock",data_start:startTime, data_add:endTime, data_action:action.ACTION}).css({"position":"relative","color":leases[action.ACTION].color});
|
||||
if(
|
||||
leases[action.ACTION].warning &&
|
||||
leases[action.ACTION].warning.time &&
|
||||
|
@ -16,14 +16,14 @@
|
||||
|
||||
define(function(require) {
|
||||
|
||||
var Humanize = require('utils/humanize');
|
||||
var LabelsUtils = require('utils/labels/utils');
|
||||
var Locale = require('utils/locale');
|
||||
var OpenNebulaVM = require('opennebula/vm');
|
||||
var Humanize = require("utils/humanize");
|
||||
var LabelsUtils = require("utils/labels/utils");
|
||||
var Locale = require("utils/locale");
|
||||
var OpenNebulaVM = require("opennebula/vm");
|
||||
var ScheduleActions = require("utils/schedule_action");
|
||||
var Status = require('utils/status');
|
||||
var TemplateUtils = require('utils/template-utils');
|
||||
var VMRemoteActions = require('utils/remote-actions');
|
||||
var Status = require("utils/status");
|
||||
var TemplateUtils = require("utils/template-utils");
|
||||
var VMRemoteActions = require("utils/remote-actions");
|
||||
|
||||
var RESOURCE = "VM";
|
||||
var XML_ROOT = "VM";
|
||||
@ -57,7 +57,7 @@ define(function(require) {
|
||||
var rtn = false;
|
||||
|
||||
if (startTime && addedEndTime) {
|
||||
var regexNumber = new RegExp('[0-9]*$','gm');
|
||||
var regexNumber = new RegExp("[0-9]*$","gm");
|
||||
var date = parseInt(startTime,10);
|
||||
var added = parseInt(addedEndTime.match(regexNumber)[0],10);
|
||||
|
||||
@ -114,7 +114,7 @@ define(function(require) {
|
||||
"padding":"8px",
|
||||
"z-index":"1",
|
||||
"min-width":"8rem",
|
||||
"font-family": '"Lato","Helvetica Neue",Helvetica,Roboto,Arial,sans-serif',
|
||||
"font-family": "\"Lato\",\"Helvetica Neue\",Helvetica,Roboto,Arial,sans-serif",
|
||||
"color":"#000",
|
||||
"font-weight": "bold"
|
||||
};
|
||||
@ -173,8 +173,10 @@ define(function(require) {
|
||||
!isNaN(parseInt(leases[action.ACTION].time)) &&
|
||||
leases[action.ACTION].color
|
||||
){
|
||||
if(checkTime(element.STIME, action.TIME)){
|
||||
rtn = $("<i/>",{class:"describeCharter fa fa-clock",data_start:element.STIME, data_add:action.TIME, data_action:action.ACTION}).css({"position":"relative","color":leases[action.ACTION].color});
|
||||
var endTime = (action.TIME.startsWith("+")? action.TIME : action.TIME - startTime).toString();
|
||||
|
||||
if(checkTime(element.STIME, endTime)){
|
||||
rtn = $("<i/>",{class:"describeCharter fa fa-clock",data_start:element.STIME, data_add:endTime, data_action:action.ACTION}).css({"position":"relative","color":leases[action.ACTION].color});
|
||||
if(
|
||||
leases[action.ACTION].warning &&
|
||||
leases[action.ACTION].warning.time &&
|
||||
|
@ -205,7 +205,7 @@ function create_base() {
|
||||
cd $DST_PATH.snap
|
||||
ln -f -s . $DST_FILE.snap ||:
|
||||
$COPY $SRC_PATH base
|
||||
qemu-img create -b $DST_FILE.snap/base -f qcow2 base.1
|
||||
qemu-img create -b $DST_FILE.snap/base -F qcow2 -f qcow2 base.1
|
||||
ln -f -s $DST_FILE.snap/base.1 $DST_PATH
|
||||
cd -
|
||||
}
|
||||
|
@ -574,20 +574,21 @@ static int parse_auth_msg(
|
||||
// <driver> <username> <passwd> [gid...]
|
||||
//--------------------------------------------------------------------------
|
||||
is.str(ar.message);
|
||||
is >> skipws;
|
||||
|
||||
if ( is.good() )
|
||||
{
|
||||
is >> driver_name >> ws;
|
||||
is >> driver_name;
|
||||
}
|
||||
|
||||
if ( !is.fail() )
|
||||
{
|
||||
is >> mad_name >> ws;
|
||||
is >> mad_name;
|
||||
}
|
||||
|
||||
if ( !is.fail() )
|
||||
{
|
||||
is >> mad_pass >> ws;
|
||||
is >> mad_pass;
|
||||
}
|
||||
|
||||
while ( is.good() )
|
||||
@ -603,7 +604,7 @@ static int parse_auth_msg(
|
||||
gr_admin = true;
|
||||
}
|
||||
|
||||
is >> tmp_gid >> ws;
|
||||
is >> tmp_gid;
|
||||
|
||||
if ( is.fail() )
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user