1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-16 22:50:10 +03:00
This commit is contained in:
Tino Vazquez 2022-06-24 13:22:24 +02:00
commit 42fe8540fc
109 changed files with 3886 additions and 1981 deletions

View File

@ -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_*/

View File

@ -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

View File

@ -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);
/**

View File

@ -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

View File

@ -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>

View 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

View File

@ -680,9 +680,6 @@ Style/RedundantBegin:
Style/ExponentialNotation:
Enabled: true
Style/SlicingWithRange:
Enabled: true
Style/AccessorGrouping:
Enabled: False

View File

@ -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:

View File

@ -1,2 +1,5 @@
# enables and configure systemd-networkd
- src: stackhpc.systemd_networkd
roles:
- stackhpc.systemd_networkd
collections:
- ansible.utils

View File

@ -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

View 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

View File

@ -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

View File

@ -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]

View File

@ -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')

View File

@ -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

View File

@ -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}"

View File

@ -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;
}

View File

@ -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:

View File

@ -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

View File

@ -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();

View File

@ -32,6 +32,7 @@ actions:
enable: true
disable: true
delete: true
edit_labels: true
# Filters - List of criteria to filter the resources

View 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

View File

@ -80,6 +80,7 @@ const MarketplaceAppCard = memo(
() =>
getUniqueLabels(LABELS).map((label) => ({
text: label,
dataCy: `label-${label}`,
stateColor: getColorFromString(label),
onClick: onClickLabel,
onDelete: enableEditLabels && onDeleteLabel,

View File

@ -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: {},
}

View File

@ -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 }
}

View File

@ -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 }) => (

View File

@ -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

View File

@ -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))

View File

@ -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 }

View File

@ -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}

View File

@ -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>

View File

@ -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

View File

@ -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,
}

View File

@ -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'

View File

@ -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,
}

View File

@ -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()

View File

@ -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,

View File

@ -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',

View File

@ -15,7 +15,6 @@
* ------------------------------------------------------------------------- */
// INFORMATION
export const REFRESH = 'refresh'
export const RENAME = 'rename'
// ATTRIBUTES

View File

@ -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',

View File

@ -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',

View File

@ -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 ?? {

View File

@ -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,
}

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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'

View File

@ -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}

View File

@ -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'

View File

@ -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} />
}

View File

@ -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>
))}
</>
)
}

View File

@ -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}`)
}}
/>
}

View File

@ -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}`)
}}
/>
</>

View File

@ -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',

View File

@ -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) => (

View File

@ -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

View File

@ -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

View File

@ -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>
</>
)

View File

@ -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 && (

View File

@ -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 && (

View File

@ -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 &&

View File

@ -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

View File

@ -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)
}

View File

@ -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] }

View File

@ -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,

View File

@ -315,7 +315,7 @@ const vmApi = oneApi.injectEndpoints({
return { params, command }
},
invalidatesTags: [VM_POOL],
invalidatesTags: (_, __, { id }) => [{ type: VM, id }, VM_POOL],
}),
actionVm: builder.mutation({
/**

View File

@ -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

View File

@ -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.
*

View File

@ -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,

View File

@ -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,
]),

View File

@ -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()
}

View File

@ -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)) {

View File

@ -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,
}

View File

@ -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]: {

View File

@ -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
}
})
}
/**

View File

@ -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
},
},
},

View File

@ -57,6 +57,7 @@ const defaults = {
defaultRegexID: /^ID: (?<id>\d+)/,
defaultRegexpEndJSON: /}$/,
defaultRegexpSplitLine: /\r|\n/,
defaultSizeRotate: '100k',
defaultAppName: appName,
defaultConfigErrorMessage: {
color: 'red',

View File

@ -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,
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 */
/* ---------------------------------------------------------------------- */

View File

@ -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())
{

View File

@ -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, &quota_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, &quota_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)

View File

@ -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, &quota_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, &quota_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, &quota_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, &quota_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, &quota_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, &quota_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, &quota_tmpl);
quota_tmpl.set(snap);
quota_tmpl.replace("VMS", 0);
Quotas::quota_del(Quotas::VM, vm_uid, vm_gid, &quota_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, &quota_tmpl);
quota_tmpl.set(snap);
quota_tmpl.replace("VMS", 0);
Quotas::quota_del(Quotas::VM, vm_uid, vm_gid, &quota_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

View File

@ -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']}"
}

View File

@ -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

View File

@ -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,

View File

@ -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|

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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 &&

View File

@ -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 &&

View File

@ -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 -
}

View File

@ -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